[Rt-commit] rt branch, 4.4/core-assets, created. rt-4.2.11-38-g791cad5
Todd Wade
todd at bestpractical.com
Fri Sep 11 09:20:30 EDT 2015
The branch, 4.4/core-assets has been created
at 791cad5ff3be5e2de6ded42cae2f5a25764fd065 (commit)
- Log -----------------------------------------------------------------
commit 791cad5ff3be5e2de6ded42cae2f5a25764fd065
Author: Todd Wade <todd at bestpractical.com>
Date: Thu Jul 23 13:52:51 2015 -0400
core RT::Extension::Assets
diff --git a/.gitignore b/.gitignore
index 855514b..e7564bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
/etc/upgrade/switch-templates-to
/etc/upgrade/time-worked-history
/etc/upgrade/upgrade-articles
+/etc/upgrade/upgrade-assets
/etc/upgrade/vulnerable-passwords
/lib/RT/Generated.pm
/Makefile
diff --git a/configure.ac b/configure.ac
index 1bcf2ba..7cbf9d0 100755
--- a/configure.ac
+++ b/configure.ac
@@ -427,6 +427,7 @@ AC_CONFIG_FILES([
etc/upgrade/switch-templates-to
etc/upgrade/time-worked-history
etc/upgrade/upgrade-articles
+ etc/upgrade/upgrade-assets
etc/upgrade/vulnerable-passwords
sbin/rt-attributes-viewer
sbin/rt-preferences-viewer
diff --git a/docs/customizing/assets_introduction.pod b/docs/customizing/assets_introduction.pod
new file mode 100644
index 0000000..f4168dd
--- /dev/null
+++ b/docs/customizing/assets_introduction.pod
@@ -0,0 +1,211 @@
+
+=head1 CONFIGURATION
+
+See L<RT_Config.pm> for documentation on the available configuration
+parameters.
+
+=head1 USAGE
+
+Assets start as a small set of fundamental record data upon which you build
+custom fields (CFs) of any type you like to describe the assets you want to
+track. By themselves, before you setup any CFs, assets are not very useful.
+
+Just like tickets are assigned to queues, assets are assigned to B<catalogs>.
+The default catalog is named "General assets", but we suggest you rename it and
+add additional catalogs to better fit your organization.
+
+You may want to use catalogs to separate assets into departments, general type
+of asset, hardware vs. software, etc. Catalogs, like queues, are generally
+easiest to work with when there's more than few but less than many dozen. The
+catalog of an asset should represent some fundamental quality of it (and many
+other assets!), that could just as easily be expressed as a custom field, but
+which is more important than other qualities for categorizing, sorting, and
+searching.
+
+=head2 Managing catalogs
+
+Catalogs are managed by RT administrators, or anyone with the L</AdminCatalog>
+right. You can find the list of catalogs, create new catalogs, and manage
+existing ones under the Tools → Configuration → Assets → Catalogs menu.
+
+Currently you need to log out and log back in to see changes to catalogs in any
+of the catalog selection dropdowns. This doesn't affect the catalog name
+displayed on individual asset pages.
+
+=head2 Adding fields
+
+You can see the current asset CFs by navigating to Admin >
+Assets > Custom Fields. From there you can use the "Create" link to create a
+new asset CF. If you know you want to create a new CF right away, you can do
+so via Admin > Assets > Custom Fields > Create.
+
+When creating a CF, be sure to select "Assets" in the "Applies To" dropdown.
+You'll also need to grant rights to the groups and/or roles which need to see
+the fields, otherwise they'll be hidden. See the following section.
+
+Similar to ticket CFs, asset custom fields are added globally or to specific
+catalogs. Only assets within those specific catalogs will have the CFs
+available. After creating a CF, you'll need to visit the "Applies To" page to
+add it to the catalogs you want or make it global.
+
+=head2 Rights
+
+There are three rights controlling basic access to assets and two for
+catalogs. Each right is grantable at the global level or individual catalog
+level, and grantable to system groups, asset roles, user groups, and individual
+users (just like ticket and queue rights).
+
+=head3 ShowAsset
+
+Allows viewing an asset record and it's core fields (but not CFs). Without
+it, no assets can be seen. Similar to ShowTicket.
+
+=head3 CreateAsset
+
+Allows creating assets and filling in the core fields (but not CFs). Without
+it, no assets can be created. Similar to CreateTicket.
+
+=head3 ModifyAsset
+
+Allows modifying existing assets and their core fields (but not CFs). Without
+it, basic asset data cannot be modified after creation. Similar to
+ModifyTicket.
+
+Most of your rights configuration will be on the CFs, and will likely need to
+be done for each CF. This lets you fine tune which fields are visible to
+individual groups and/or roles of users. Relevant CF rights are
+B<SeeCustomField> and B<ModifyCustomField>.
+
+Rights related to assets may also come from the L</Lifecycle statuses>
+configuration and restrict status transitions.
+
+=head3 ShowCatalog
+
+Allows seeing a catalog's name and other details when associated with assets.
+Without it, users will see "[a hidden catalog]" or a blank space where the
+catalog name would normally be. Similar to SeeQueue.
+
+=head3 AdminCatalog
+
+Allows creating new catalogs and modifying all aspects of existing catalogs,
+including changing the CFs associated with the catalog, granting/revoking
+rights, and adding/removing role members. This right should only be granted to
+administrators of RT. Similar to AdminQueue.
+
+=head3 Typical configuration
+
+A typical configuration grants the system Privileged group the following:
+B<ShowAsset>, B<CreateAsset>, B<ModifyAsset>, and B<ShowCatalog> globally, and
+B<SeeCustomField> and B<ModifyCustomField> globally on all asset CFs.
+
+If you want self service users (Unprivileged) to be able to view the assets
+they hold, grant the Held By role B<ShowAsset> and B<ShowCatalog> globally and
+B<SeeCustomField> on the necessary asset CFs.
+
+=head2 People and Roles
+
+Just like tickets, assets have various roles which users and groups may be
+assigned to. The intended usages of these roles are described below, but
+you're free to use them for whatever you'd like, of course.
+
+The roles provide ways to keep track of who is involved with each asset, as
+well as providing a place to grant rights that depend on the user's association
+with each asset.
+
+In addition to adding people to individual asset roles, you can also add role
+members at an entire catalog level. These catalog-level roles are useful in
+cases when you might have an entire catalog of assets for which the same people
+should be the Contacts, or which are Held By the same group. Unlike tickets
+where the queue watchers are invisible, catalog role members are visible
+because assets are generally much longer lived than tickets. When a problem
+with an asset arises, it's easier to see who to create a ticket for. On
+individual asset pages, catalog role members are shown with the text "(via this
+asset's catalog)" following each name.
+
+=head3 Owner
+
+The person responsible for the asset, perhaps the purchaser or manager.
+
+Restricted to a single user. Not available at a catalog level.
+
+=head3 Held By
+
+The person or people who physically possess the asset or are actively using the
+asset (if it isn't physical). This may be the same as the Contacts or may be
+different. For example, a computer workstation may be "held by" a university
+professor, but the contact may be the IT staff member responsible for all
+assets in the professor's department. This role is most similar to Requestor
+on tickets, although not equivalent.
+
+May be multiple users and/or groups.
+
+=head3 Contact
+
+The person or people who should be contacted with questions, problems,
+notifications, etc. about the asset. Contacts share some of the same intended
+usages of both Requestors and Ccs on tickets.
+
+May be multiple users and/or groups.
+
+=head2 Lifecycle statuses
+
+One of the basic asset fields is "Status". Similar to tickets, the valid
+statuses and their transitions and actions can be customized via RT's standard
+Lifecycles configuration (see "Lifecycles" in F<RT_Config.pm>). The default
+lifecycle is named "assets". You're free to modify it as much as you'd like,
+or add your own lifecycles. Each catalog may have its own lifecycle.
+
+For the default "assets" configuration, see F<etc/Assets_Config.pm>.
+
+=head2 Field organization
+
+=head3 Groupings
+
+You can organize your asset CFs into visual and logical "groupings" as you see
+fit. These groupings appear as separate boxes on the asset display page and
+become separate pages for editing (showing up in the per-asset menu).
+
+By default your CFs will appear in a B<Custom Fields> box on the asset display
+page and will be editable from a box of the same name on the B<Basics> editing
+page.
+
+Using the C<%CustomFieldGroupings> option (documented in F<etc/RT_Config.pm>),
+you can move individual CFs by name into one of the four built-in groupings
+(B<Basics>, B<People>, B<Dates>, and B<Links>) or create your own just by
+naming it. An example, assuming a date CF named "Purchased" and two "enter one
+value" CFs named "Weight" and "Color":
+
+ # In etc/RT_SiteConfig.pm
+ Set(%CustomFieldGroupings,
+ 'RT::Asset' => {
+ 'Dates' => ['Purchased'],
+ 'Physical Properties' => ['Weight', 'Color'],
+ },
+ );
+
+This configuration snippet will move all three CFs out of the generic B<Custom
+Fields> box and into the B<Dates> box and a new box titled B<Physical
+Properties>. The "Purchased" CF will be editable from the Dates page and a new
+page titled "Physical Properties" will appear in the menu to allow editing of
+the "Weight" and "Color" CFs.
+
+=head3 Ordering
+
+Within a box, CFs come after any built-in asset fields such as Name,
+Description, Created, Last Updated, etc. The CFs themselves are ordered
+according to the sorting seen (and adjustable) on the global Asset Custom
+Fields page (Tools → Configuration → Global → Custom Fields → Assets) and the
+individual catalog Custom Fields pages (Tools → Configuration → Assets →
+Catalogs → (Pick one) → Custom Fields).
+
+Global asset CFs may be intermixed with per-catalog CFs with ordering.
+
+=head2 Importing existing data
+
+Another extension, L<RT::Extension::Assets::Import::CSV> provides tools
+to import new and update existing assets from a CSV dump. Its
+configuration lets you map the fields in the CSV to the asset fields
+you've already created in RT. L<RT::Extension::Assets::AppleGSX> also
+provides tools for looking up data associated with an Apple product.
+
+=cut
diff --git a/docs/customizing/assets_tutorial.pod b/docs/customizing/assets_tutorial.pod
new file mode 100644
index 0000000..43f1aec
--- /dev/null
+++ b/docs/customizing/assets_tutorial.pod
@@ -0,0 +1,327 @@
+
+=head1 Introduction
+
+This is a basic tutorial for setting up asset tracking in RT using Best
+Practical's Assets extension. At the end, you'll have a basic configuration
+that lets you add assets, search for them, link them to tickets, etc.
+
+=begin HTML
+
+<p><img src="http://static.bestpractical.com/images/assets/asset-search.png"
+alt="Asset Search Results" /></p>
+
+=end HTML
+
+You can follow along with the tutorial and try setting things up yourself to
+get a feel for all of the administrative controls. If you want to get a jump
+start, the files to set up this basic configuration are provided in the
+L<RT::Extension::Assets> distribution in the F<etc> directory. For
+configuration, look in F<etc/Tutorial_Configuration.txt>. You can copy all or
+part of it and paste it into your F<RT_SiteConfig.pm>.
+
+To load the test catalog, custom fields, and users, follow the
+installation instructions in L<RT::Extension::Assets/INSTALLATION>, then
+run the following from your RT directory:
+
+ sbin/rt-setup-database --action insert --datafile \
+ local/plugins/RT-Extension-Assets/etc/tutorialdata
+
+This will change the default catalog name, create some users, and give those
+users asset permissions. Only asset rights are granted, so you need to grant
+additional rights if you want to experiment with creating tickets and linking
+assets. The initial user passwords are 'password'. You should only run this on
+a test RT instance, as it is not intended to be used for configuring a
+production system.
+
+=head1 Getting Started
+
+Install the extension following the instructions and some new tables will be
+added to your RT database and the assets code will be installed. As with all
+extensions, first add C<RT::Extension::Assets> to your C<@Plugins> line.
+
+There are a few configuration options you might set before starting. Assets
+offers a C<$DefaultCatalog> feature that works similar to RT's
+L<DefaultQueue|http://bestpractical.com/docs/rt/latest/RT_Config.html#DefaultQueue>,
+but you can probably skip it for now since you don't have any catalogs yet.
+
+More interesting are some optional portlets you can activate to add asset data
+to RT's pages. MyAssets and FindAsset portlets are available for placement on
+RT at a Glance or in dashboards and a UserAssets portlet is available for the
+user summary pages.
+
+These portlets are fairly self-explanatory and you can add them by finding
+C<$HomepageComponents> and C<@UserSummaryPortlets> respectively in
+F<RT_Config.pm>, copying to F<RT_SiteConfig.pm>, and adding the portlets you
+want. There are also examples in the tutorial sample configuration file. Note
+that C<$HomepageComponents> makes the portlets available, but doesn't put them
+on RT at a Glance. To add them, just click the Edit link on the upper right-hand
+corner of the homepage. C<@UserSummaryPortlets> does automatically add the
+"Assigned Assets" portlet to the User Summary page. It will appear based on the
+position in the configuration, so just place it in the list where you want it
+to appear.
+
+Once you have your configuration complete, restart your server and you're ready
+to go.
+
+=head1 Catalogs
+
+For the initial configuration, we'll log in as RT's root user so we have full
+rights on the asset configuration. You could also create an "Asset Admin" group
+and assign appropriate rights to allow other users to manage catalogs.
+
+When you log in you'll see a new Assets menu, but before looking there we need
+to look at catalogs and some other configuration. Catalogs are to assets what
+queues are to tickets, so if you've used RT, the relationship should be fairly
+familiar. Similar to the General queue, a "General assets" catalog is provided
+to get you started. You can see it at Admin > Assets > Catalogs.
+
+We're going to use the default, but change it to a name more appropriate for
+our use. Clicking on the asset name brings us to the catalog edit page and we
+can update the name to "IT Department Assets". You can update the description
+if you like as well.
+
+=begin HTML
+
+<p><img src="http://static.bestpractical.com/images/assets/edit-catalog.png"
+alt="Edit Catalog" /></p>
+
+=end HTML
+
+You'll also notice that catalogs have a lifecycle just like queues. The assets
+extension comes with a default assets lifecycle, but just like queues you can
+create new ones with custom statuses and other configuration to allow RT to
+reflect the states of your assets.
+
+You can find the asset lifecycle in the asset configuration file in your RT
+installation at:
+
+ local/plugins/RT-Extension-Assets/etc/Assets_Config.pm
+
+The initial statuses are new, allocated, in-use, recycled, stolen, and deleted.
+Depending on your process, you might add new ones like surplussed, donated, or
+in-repair. To create a new asset lifecycle, just copy the default into
+F<RT_SiteConfig.pm>, replace the top-level "assets" key with a new name, and
+make your changes.
+
+=head1 Asset Custom Fields
+
+Next we need to create some custom fields to hold our asset metadata. You can
+find asset custom fields at Admin > Assets > Custom Fields and they work just
+like custom fields for other RT objects.
+
+=begin HTML
+
+<p><img src="http://static.bestpractical.com/images/assets/asset-cfs.png"
+alt="Asset Custom Fields" /></p>
+
+=end HTML
+
+The extension will automatically provide some core values for your assets. Each
+asset can have a Name and Description and, like tickets, they have statuses
+based on the lifecycle configuration. You can use Name and Description however
+you want and they are not required. However, many of the asset pages use these
+fields so it's best to provide a descriptive name to make it easy for people
+working in RT to identify the asset quickly. The manufacturer's product name
+can be convenient (e.g., '15" Macbook Pro').
+
+Assets come with three user fields you can associate with an asset: Owner, Held
+By, and Contact. These are provided to cover different types of assets, from
+laptops to servers to software, and different asset management situations.
+Owner can hold the user who bought the asset, maybe the head of the department
+where the budget came from. Held by is who the asset is assigned to. Laptops
+are assigned to a user and servers might be held (or managed) by the system
+administrators. Contact can be used to set a manager who might need to know
+about needed system updates or equipment with expiring support. Like tickets,
+these roles give you places to attach rights, so use them however they work
+best for you.
+
+Any other information you want to track will need custom fields. We'll start
+with a few basic fields:
+
+=over
+
+=item * Serial Number (enter one value)
+
+The serial number from the asset.
+
+=item * Tracking Number (enter one value)
+
+An internal tracking number. RT will assign an asset ID as well, but you may
+have other systems to integrate with or already have a way to assign asset ids
+for accounting purposes.
+
+=item * Manufacturer (dropdown)
+
+Company that made the asset.
+
+=item * Type (dropdown)
+
+Is it a laptop, server, or cell phone?
+
+=item * Issue Date (date)
+
+When the asset was given to the owner (or held by) person.
+
+Assets keep a transaction history like tickets, so you may be able pull this
+information from the "owner set to X" transaction. Creating a separate field
+makes it easier to report on.
+
+=item * Support Expiration (date)
+
+When the current support contract expires.
+
+=back
+
+=head1 Custom Field Grouping
+
+Any custom fields you create will be displayed on the asset display page in a
+default "Custom Fields" section. That may be sufficient, but assets also
+supports RT's new custom field grouping feature, so we can group together some
+similar custom fields and give them a custom name. If we add the following to
+F<RT_SiteConfig.pm>:
+
+ Set(%CustomFieldGroupings,
+ 'RT::Asset' => {
+ 'Asset Details' => ['Serial Number', 'Manufacturer', 'Type', 'Tracking Number'],
+ 'Dates' => ['Support Expiration', 'Issue Date'],
+ },
+ );
+
+and restart RT, the dates will be tacked on the end of the Dates portlet and we
+get an Asset Details label on the other custom fields.
+
+=begin HTML
+
+<p><img
+src="http://static.bestpractical.com/images/assets/asset-date-details.png"
+alt="Asset Date and Details Display" /></p>
+
+=end HTML
+
+=head1 Asset Rights
+
+Now we've got the basic configuration in place to start recording asset data.
+Next we need to assign some rights so people can view and edit asset
+information. Our staff are all privileged users so we'll grant all view and
+modify rights on our catalog to the Privileged role. We'll also include rights
+to view and modify the catalog's custom fields, although you could set these
+rights individually on each custom field if you wanted to allow users to see
+some but not others.
+
+Similar to queues, you can set rights at the catalog level. Go to Admin >
+Assets > Catalogs and click on the catalog you want to edit. Click Group Rights
+in the submenu to assign asset rights to groups like the system Privileged
+group.
+
+=begin HTML
+
+<p><img src="http://static.bestpractical.com/images/assets/catalog-rights.png"
+alt="Catalog Rights" /></p>
+
+=end HTML
+
+We also want unprivileged users to be able to see their own assets to make it
+easier to submit support requests. To give them just the Name and Description
+on their own assets, we can grant SeeAssets and SeeCatalogs on the catalog to
+the Held By role.
+
+All of the asset rights are described in the Assets documentation. You can get
+much more detailed and fine-grained than this example, allowing selected groups
+and users to view and modify multiple different asset custom fields across many
+different catalogs.
+
+=head1 Working with Assets
+
+So now that we have all of that configuration done, what can we do? Here are a
+few scenarios to give you some ideas.
+
+=head2 Add Assets to Your Catalogs
+
+To start, staff can now start adding assets to RT allowing you to manage what
+you have, what state it's in, who currently has it, and when support expires.
+You could set up an intake process to get new assets added as they come in, and
+eventually have statuses updated as they are assigned, used, and eventually
+cycled out.
+
+If you already have an asset database, even something simple like a
+spreadsheet, you may be able to do an initial bulk import. Best Practical has
+released L<RT::Extension::Assets::Import::CSV> which is a CSV import tool to
+help you with this.
+
+=head2 Track Assets
+
+Your staff can now easily track work on assets by linking RT tickets to the
+assets. Assume you have an issue with an asset, like a server needs a new power
+supply. Your staff can use the asset search page to find the server. You'll
+notice that the RT search box is context sensitive, so when you're on an asset
+page, the search changes to Search Assets and you can search with that as well.
+
+Once you locate the server asset record, in the Actions menu you'll find
+"Create linked ticket", which does just that. You select the queue and which
+user to use from the asset as the Requestor, and you land on the ticket create
+page with some information pre-filled.
+
+=begin HTML
+
+<p><img
+src="http://static.bestpractical.com/images/assets/asset-ticket-create.png"
+alt="Create Ticket for Asset Work" /></p>
+
+=end HTML
+
+As you can see in the screenshot, when you create a ticket with a linked asset,
+you get an asset portlet on the create page and on the ticket display page as
+well. If you navigate back to the asset, you'll see a link back to the ticket
+in the Links section there. This gives you a record of all the tickets that
+have been opened against this asset. If this is a common scenario for you, you
+might even add a custom field on the ticket with the vendor tracking number of
+the repair. During the repair, you might flip the asset to an 'in-repair'
+status. Then when the ticket is resolved, flip it back to 'in-use'.
+
+=head2 End User Asset Tickets
+
+If an end user contacts us with some problems with their laptop, RT makes it
+easy to find the correct laptop record and create a ticket for them. Since our
+support staff do this frequently, they have added the Find User portlet to
+their RT at a glance page and can quickly search for the user and go to their
+User Summary page (new in RT 4.2).
+
+We have added the Assigned Assets portlet to the User Summary page, so the
+laptop is right there on the page when we find the user. We can just click on
+the asset, then use the "Create linked ticket" action as before to create the
+new repair ticket.
+
+=head2 End User Self Service
+
+Assume we already assign passwords to our unprivileged users so they can use
+RT's self service interface to submit tickets and they have basic permissions
+to do so (SeeQueue on the designated queue, CreateTicket, etc.). Since we've
+given some asset rights to unprivileged users, they can use RT's Self Service
+interface to find their assets (e.g., laptops, cell phones, etc.) when
+submitting support requests.
+
+When they log into the self service interface, they will see an Assets menu
+that takes them to a page displaying assets assigned to them. In our example
+configuration, this is based on the Held by setting we set when we gave out the
+laptop. When they navigate to the asset, they will see the Actions menu with
+the same "Create linked ticket" action our staff uses. When they click on that,
+they'll end up on the simplified ticket create page for self service. When the
+ticket is created, the laptop will already be linked to it, saving our staff
+the work.
+
+=begin HTML
+
+<p><img
+src="http://static.bestpractical.com/images/assets/asset-ticket-create-selfservice.png"
+alt="Self Service Ticket for Asset Work" /></p>
+
+=end HTML
+
+=head1 Summary
+
+This tutorial is only a quick overview showing how the assets extension can
+help you track assets. There are many more features you'll find as you explore
+the assets interface, like stacking multiple assets on a single ticket, bulk
+update features similar to tickets, and the search interface. Have fun!
+
+=cut
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index fd9e971..bb7c095 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1644,7 +1644,138 @@ Set($HideArticleSearchOnReplyCreate, 0);
=back
+=head1 Assets
+=over 4
+
+=item C<@AssetQueues>
+
+This should be a list of names of queues whose tickets should always
+display the "Assets" box. This is useful for queues which deal
+primarily with assets, as it provides a ready box to link an asset to
+the ticket, even when the ticket has no related assets yet.
+
+=cut
+
+# Set(@AssetQueues, ());
+
+=item C<$DefaultCatalog>
+
+This provides the default catalog after a user initially logs in.
+However, the default catalog is "sticky," and so will remember the
+last-selected catalog thereafter.
+
+=cut
+
+# Set($DefaultCatalog, 'General assets');
+
+=item C<$AssetSearchFields>
+
+Specifies which fields of L<RT::Asset> to match against and how to match
+each field when performing a quick search on assets. Valid match
+methods are LIKE, STARTSWITH, ENDSWITH, =, and !=. Valid search fields
+are id, Name, Description, or custom fields, which are specified as
+"CF.1234" or "CF.Name"
+
+=cut
+
+Set($AssetSearchFields, {
+ id => '=',
+ Name => 'LIKE',
+ Description => 'LIKE',
+}) unless $AssetSearchFields;
+
+=item C<$AssetSearchFormat>
+
+The format that results of the asset search are displayed with. This is
+either a string, which will be used for all catalogs, or a hash
+reference, keyed by catalog's name/id. If a hashref and neither name or
+id is found therein, falls back to the key ''.
+
+If you wish to use the multiple catalog format, your configuration would look
+something like:
+
+ Set($AssetSearchFormat, {
+ 'General assets' => q[Format String for the General Assets Catalog],
+ 8 => q[Format String for Catalog 8],
+ '' => q[Format String for any catalogs not listed explicitly],
+ });
+
+=cut
+
+# loc('Related tickets')
+Set($AssetSearchFormat, q[
+ '<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
+ Description,
+ '__Status__ (__Catalog__)/TITLE:Status',
+ Owner,
+ HeldBy,
+ Contacts,
+ '__ActiveTickets__ __InactiveTickets__/TITLE:Related tickets',
+]) unless $AssetSearchFormat;
+
+=item C<$AssetSummaryFormat>
+
+The information that is displayed on ticket display pages about assets
+related to the ticket. This is displayed in a table beneath the asset
+name.
+
+=cut
+
+Set($AssetSummaryFormat, q[
+ '<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
+ Description,
+ '__Status__ (__Catalog__)/TITLE:Status',
+ Owner,
+ HeldBy,
+ Contacts,
+ '__ActiveTickets__ __InactiveTickets__/TITLE:Related tickets',
+]) unless $AssetSummaryFormat;
+
+=item C<$AssetSummaryRelatedTicketsFormat>
+
+The information that is displayed on ticket display pages about tickets
+related to assets related to the ticket. This is displayed as a list of
+tickets underneath the asset properties.
+
+=cut
+
+Set($AssetSummaryRelatedTicketsFormat, q[
+ '<a href="__WebPath__/Ticket/Display.html?id=__id__">__id__</a>',
+ '(__OwnerName__)',
+ '<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>',
+ QueueName,
+ Status,
+]) unless $AssetSummaryRelatedTicketsFormat;
+
+=item C<%AdminSearchResultFormat>
+
+The C<Catalogs> key of this standard RT configuration option (see
+L<RT_Config/%AdminSearchResultFormat>) controls how catalogs are
+displayed in their list in the admin pages.
+
+=cut
+
+Set(%AdminSearchResultFormat,
+ Catalogs =>
+ q{'<a href="__WebPath__/Admin/Assets/Catalogs/Modify.html?id=__id__">__id__</a>/TITLE:#'}
+ .q{,'<a href="__WebPath__/Admin/Assets/Catalogs/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
+ .q{,__Description__,__Lifecycle__,__Disabled__},
+) unless $AdminSearchResultFormat{Catalogs};
+
+=item C<$AssetBasicCustomFieldsOnCreate>
+
+Specify a list of Asset custom fields to show in "Basics" widget on create.
+
+e.g.
+
+Set( $AssetBasicCustomFieldsOnCreate, [ 'foo', 'bar' ] );
+
+=cut
+
+# Set($AssetBasicCustomFieldsOnCreate, undef );
+
+=back
=head2 Message box properties
@@ -2359,8 +2490,6 @@ Set($TimeInICal, 0);
=back
-
-
=head1 Cryptography
A complete description of RT's cryptography capabilities can be found in
@@ -2875,6 +3004,52 @@ Set(%Lifecycles,
'deleted -> open' => { label => 'Undelete', }, # loc{label}
],
},
+ assets => {
+ type => "asset",
+ initial => [
+ 'new' # loc
+ ],
+ active => [
+ 'allocated', # loc
+ 'in-use' # loc
+ ],
+ inactive => [
+ 'recycled', # loc
+ 'stolen', # loc
+ 'deleted' # loc
+ ],
+
+ defaults => {
+ on_create => 'new',
+ },
+
+ transitions => {
+ '' => [qw(new allocated in-use)],
+ new => [qw(allocated in-use stolen deleted)],
+ allocated => [qw(in-use recycled stolen deleted)],
+ "in-use" => [qw(allocated recycled stolen deleted)],
+ recycled => [qw(allocated)],
+ stolen => [qw(allocated)],
+ deleted => [qw(allocated)],
+ },
+ rights => {
+ '* -> *' => 'ModifyAsset',
+ },
+ actions => {
+ '* -> allocated' => {
+ label => "Allocate" # loc
+ },
+ '* -> in-use' => {
+ label => "Now in-use" # loc
+ },
+ '* -> recycled' => {
+ label => "Recycle" # loc
+ },
+ '* -> stolen' => {
+ label => "Report stolen" # loc
+ },
+ },
+ },
);
diff --git a/etc/acl.Pg b/etc/acl.Pg
index a659d8e..3c05399 100644
--- a/etc/acl.Pg
+++ b/etc/acl.Pg
@@ -80,6 +80,35 @@ sub acl {
}
}
return (@acls);
+
+{ # START assets ACL
+ my @tables = qw (
+ assets_id_seq
+ Assets
+ catalogs_id_seq
+ Catalogs
+ );
+
+ my $db_user = RT->Config->Get('DatabaseUser');
+
+ my $sequence_right
+ = ( $dbh->{pg_server_version} >= 80200 )
+ ? "USAGE, SELECT, UPDATE"
+ : "SELECT, UPDATE";
+
+ foreach my $table (@tables) {
+ # Tables are upper-case, sequences are lowercase in @tables
+ if ( $table =~ /^[a-z]/ ) {
+ push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+ }
+ else {
+ push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
+ }
+ }
+
+} # END Assets ACL
+
+ return (@acls);
}
1;
diff --git a/etc/initialdata b/etc/initialdata
index dd1daf5..f876940 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -886,3 +886,33 @@ Hour: { $SubscriptionObj->SubValue('Hour') }
},
},
);
+
+require RT::Asset;
+# Create global role groups
+push @Final, sub {
+ foreach my $type (RT::Asset->Roles) {
+ next if $type eq "Owner"; # There's a core global role group for Owner
+
+ my $group = RT::Group->new( RT->SystemUser );
+ my ($ok, $msg) = $group->CreateRoleGroup(
+ Object => RT->System,
+ Name => $type,
+ InsideTransaction => 0,
+ );
+ RT->Logger->error("Couldn't create global asset role group '$type': $msg")
+ unless $ok;
+ }
+};
+
+# Create default catalog
+push @Final, sub {
+ my $catalog = RT::Catalog->new( RT->SystemUser );
+ my ($ok, $msg) = $catalog->Create(
+ Name => "General assets",
+ Description => "The default catalog",
+ );
+ RT->Logger->error("Couldn't create default catalog 'General assets': $msg")
+ unless $ok;
+};
+
+1;
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index effefc5..950c696 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -487,3 +487,35 @@ Created DATE,
LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
LastUpdated DATE
);
+CREATE SEQUENCE Assets_seq;
+CREATE TABLE Assets (
+ id NUMBER(11,0) CONSTRAINT Assets_key PRIMARY KEY,
+ Name varchar2(255) DEFAULT '',
+ Catalog NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Status varchar2(64) DEFAULT '',
+ Description varchar2(255) DEFAULT '',
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+CREATE INDEX AssetsName ON Assets (LOWER(Name));
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE SEQUENCE Catalogs_seq;
+CREATE TABLE Catalogs (
+ id NUMBER(11,0) CONSTRAINT Catalogs_key PRIMARY KEY,
+ Name varchar2(255) DEFAULT '',
+ Lifecycle varchar2(32) DEFAULT 'assets',
+ Description varchar2(255) DEFAULT '',
+ Disabled NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+CREATE INDEX CatalogsName ON Catalogs (LOWER(Name));
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/schema.Pg b/etc/schema.Pg
index e5e2a04..6fc6aaa 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -719,3 +719,37 @@ LastUpdated TIMESTAMP NULL,
PRIMARY KEY (id)
);
+CREATE SEQUENCE assets_id_seq;
+CREATE TABLE Assets (
+ id integer DEFAULT nextval('assets_id_seq'),
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog integer NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator integer NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy integer NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX AssetsName ON Assets (LOWER(Name));
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE SEQUENCE catalogs_id_seq;
+CREATE TABLE Catalogs (
+ id integer DEFAULT nextval('catalogs_id_seq'),
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled integer NOT NULL DEFAULT 0,
+ Creator integer NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy integer NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX CatalogsName ON Catalogs (LOWER(Name));
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index c50e5b1..8364b33 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -519,3 +519,33 @@ Created TIMESTAMP NULL,
LastUpdatedBy integer NOT NULL DEFAULT 0,
LastUpdated TIMESTAMP NULL
);
+CREATE TABLE Assets (
+ id INTEGER PRIMARY KEY,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog int(11) NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL
+);
+
+CREATE INDEX AssetsName on Assets (Name);
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE TABLE Catalogs (
+ id INTEGER PRIMARY KEY,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled int2 NOT NULL DEFAULT 0,
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL
+);
+
+CREATE INDEX CatalogsName on Catalogs (Name);
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/schema.mysql b/etc/schema.mysql
index da14e72..b12127b 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -508,3 +508,35 @@ CREATE TABLE ObjectClasses (
LastUpdated datetime default NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE Assets (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog int(11) NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created datetime DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated datetime DEFAULT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX AssetsName ON Assets (Name);
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE TABLE Catalogs (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled int2 NOT NULL DEFAULT 0,
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created datetime DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated datetime DEFAULT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX CatalogsName ON Catalogs (Name);
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/upgrade/4.3.10/content b/etc/upgrade/4.3.10/content
new file mode 100644
index 0000000..93d0e9a
--- /dev/null
+++ b/etc/upgrade/4.3.10/content
@@ -0,0 +1,31 @@
+
+require RT::Asset;
+
+# Create global role groups
+push @Final, sub {
+ foreach my $type (RT::Asset->Roles) {
+ next if $type eq "Owner"; # There's a core global role group for Owner
+
+ my $group = RT::Group->new( RT->SystemUser );
+ my ($ok, $msg) = $group->CreateRoleGroup(
+ Object => RT->System,
+ Name => $type,
+ InsideTransaction => 0,
+ );
+ RT->Logger->error("Couldn't create global asset role group '$type': $msg")
+ unless $ok;
+ }
+};
+
+# Create default catalog
+push @Final, sub {
+ my $catalog = RT::Catalog->new( RT->SystemUser );
+ my ($ok, $msg) = $catalog->Create(
+ Name => "General assets",
+ Description => "The default catalog",
+ );
+ RT->Logger->error("Couldn't create default catalog 'General assets': $msg")
+ unless $ok;
+};
+
+1;
diff --git a/etc/upgrade/4.3.10/schema.Oracle b/etc/upgrade/4.3.10/schema.Oracle
new file mode 100644
index 0000000..646b53e
--- /dev/null
+++ b/etc/upgrade/4.3.10/schema.Oracle
@@ -0,0 +1,33 @@
+
+CREATE SEQUENCE Assets_seq;
+CREATE TABLE Assets (
+ id NUMBER(11,0) CONSTRAINT Assets_key PRIMARY KEY,
+ Name varchar2(255) DEFAULT '',
+ Catalog NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Status varchar2(64) DEFAULT '',
+ Description varchar2(255) DEFAULT '',
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+CREATE INDEX AssetsName ON Assets (LOWER(Name));
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE SEQUENCE Catalogs_seq;
+CREATE TABLE Catalogs (
+ id NUMBER(11,0) CONSTRAINT Catalogs_key PRIMARY KEY,
+ Name varchar2(255) DEFAULT '',
+ Lifecycle varchar2(32) DEFAULT 'assets',
+ Description varchar2(255) DEFAULT '',
+ Disabled NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+CREATE INDEX CatalogsName ON Catalogs (LOWER(Name));
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/upgrade/4.3.10/schema.Pg b/etc/upgrade/4.3.10/schema.Pg
new file mode 100644
index 0000000..7194261
--- /dev/null
+++ b/etc/upgrade/4.3.10/schema.Pg
@@ -0,0 +1,35 @@
+
+CREATE SEQUENCE assets_id_seq;
+CREATE TABLE Assets (
+ id integer DEFAULT nextval('assets_id_seq'),
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog integer NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator integer NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy integer NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX AssetsName ON Assets (LOWER(Name));
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE SEQUENCE catalogs_id_seq;
+CREATE TABLE Catalogs (
+ id integer DEFAULT nextval('catalogs_id_seq'),
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled integer NOT NULL DEFAULT 0,
+ Creator integer NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy integer NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX CatalogsName ON Catalogs (LOWER(Name));
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/upgrade/4.3.10/schema.SQLite b/etc/upgrade/4.3.10/schema.SQLite
new file mode 100644
index 0000000..38c7ee0
--- /dev/null
+++ b/etc/upgrade/4.3.10/schema.SQLite
@@ -0,0 +1,31 @@
+
+CREATE TABLE Assets (
+ id INTEGER PRIMARY KEY,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog int(11) NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL
+);
+
+CREATE INDEX AssetsName on Assets (Name);
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE TABLE Catalogs (
+ id INTEGER PRIMARY KEY,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled int2 NOT NULL DEFAULT 0,
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created timestamp DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated timestamp DEFAULT NULL
+);
+
+CREATE INDEX CatalogsName on Catalogs (Name);
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/upgrade/4.3.10/schema.mysql b/etc/upgrade/4.3.10/schema.mysql
new file mode 100644
index 0000000..98c9f2b
--- /dev/null
+++ b/etc/upgrade/4.3.10/schema.mysql
@@ -0,0 +1,33 @@
+
+CREATE TABLE Assets (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Catalog int(11) NOT NULL DEFAULT 0,
+ Status varchar(64) NOT NULL DEFAULT '',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created datetime DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated datetime DEFAULT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX AssetsName ON Assets (Name);
+CREATE INDEX AssetsStatus ON Assets (Status);
+CREATE INDEX AssetsCatalog ON Assets (Catalog);
+
+CREATE TABLE Catalogs (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ Name varchar(255) NOT NULL DEFAULT '',
+ Lifecycle varchar(32) NOT NULL DEFAULT 'assets',
+ Description varchar(255) NOT NULL DEFAULT '',
+ Disabled int2 NOT NULL DEFAULT 0,
+ Creator int(11) NOT NULL DEFAULT 0,
+ Created datetime DEFAULT NULL,
+ LastUpdatedBy int(11) NOT NULL DEFAULT 0,
+ LastUpdated datetime DEFAULT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX CatalogsName ON Catalogs (Name);
+CREATE INDEX CatalogsDisabled ON Catalogs (Disabled);
diff --git a/etc/upgrade/upgrade-assets.in b/etc/upgrade/upgrade-assets.in
new file mode 100644
index 0000000..d46980b
--- /dev/null
+++ b/etc/upgrade/upgrade-assets.in
@@ -0,0 +1,101 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use 5.10.1;
+use strict;
+use warnings;
+
+use lib "@LOCAL_LIB_PATH@";
+use lib "@RT_LIB_PATH@";
+
+use RT::Interface::CLI qw(Init);
+Init();
+
+my $db_name = RT->Config->Get('DatabaseName');
+my $db_type = RT->Config->Get('DatabaseType');
+
+my $dbh = $RT::Handle->dbh;
+
+my $found_assets_tables;
+foreach my $name ( $RT::Handle->_TableNames ) {
+ next unless $name =~ /^RTx_/i;
+ $found_assets_tables->{lc $name}++;
+}
+
+unless ( $found_assets_tables->{rtxassets} && $found_assets_tables->{rtxcatalogs} ) {
+ warn "Could not find RT::Extension::Assets data to migrate";
+ exit;
+}
+
+{ # port over Catalogs
+ my @columns = qw(id Name Lifecycle Description Disabled Creator Created LastUpdatedBy LastUpdated);
+ copy_tables('RTxCatalogs','Catalogs',\@columns);
+
+}
+
+
+{ # port over Assets
+ my @columns = qw(id Name Catalog Status Description Creator Created LastUpdatedBy LastUpdated);
+ copy_tables('RTxAssets','Assets',\@columns);
+}
+
+sub copy_tables {
+ my ($source, $dest, $columns) = @_;
+ my $column_list = join(', ',@$columns);
+ my $sql;
+ # SQLite: http://www.sqlite.org/lang_insert.html
+ if ( $db_type eq 'mysql' || $db_type eq 'SQLite' ) {
+ $sql = "insert into $dest ($column_list) select $column_list from $source";
+ }
+ # Oracle: http://www.adp-gmbh.ch/ora/sql/insert/select_and_subquery.html
+ elsif ( $db_type eq 'Pg' || $db_type eq 'Oracle' ) {
+ $sql = "insert into $dest ($column_list) (select $column_list from $source)";
+ }
+ $RT::Logger->debug($sql);
+ $dbh->do($sql);
+}
diff --git a/lib/RT.pm b/lib/RT.pm
index 4147992..339549f 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -477,6 +477,10 @@ sub InitClasses {
require RT::Topics;
require RT::Link;
require RT::Links;
+ require RT::Catalog;
+ require RT::Catalogs;
+ require RT::Asset;
+ require RT::Assets;
_BuildTableAttributes();
diff --git a/lib/RT/Asset.pm b/lib/RT/Asset.pm
new file mode 100644
index 0000000..d2fe338
--- /dev/null
+++ b/lib/RT/Asset.pm
@@ -0,0 +1,647 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+use 5.10.1;
+
+package RT::Asset;
+use base 'RT::Record';
+
+use Role::Basic "with";
+with "RT::Record::Role::Status",
+ "RT::Record::Role::Links",
+ "RT::Record::Role::Roles" => {
+ -rename => {
+ # We provide ACL'd wraps of these.
+ AddRoleMember => "_AddRoleMember",
+ DeleteRoleMember => "_DeleteRoleMember",
+ RoleGroup => "_RoleGroup",
+ },
+ };
+
+require RT::Catalog;
+require RT::CustomField;
+require RT::URI::asset;
+
+=head1 NAME
+
+RT::Asset - Represents a single asset record
+
+=cut
+
+sub LifecycleColumn { "Catalog" }
+
+# Assets are primarily built on custom fields
+RT::CustomField->RegisterLookupType( CustomFieldLookupType() => 'Assets' );
+RT::CustomField->RegisterBuiltInGroupings(
+ 'RT::Asset' => [qw( Basics Dates People Links )]
+);
+
+# loc('Owner')
+# loc('HeldBy')
+# loc('Contact')
+for my $role ('Owner', 'HeldBy', 'Contact') {
+ state $i = 1;
+ RT::Asset->RegisterRole(
+ Name => $role,
+ EquivClasses => ["RT::Catalog"],
+ SortOrder => $i++,
+ ( $role eq "Owner"
+ ? ( Single => 1,
+ ACLOnlyInEquiv => 1, )
+ : () ),
+ );
+}
+
+=head1 DESCRIPTION
+
+An Asset is a small record object upon which zero to many custom fields are
+applied. The core fields are:
+
+=over 4
+
+=item id
+
+=item Name
+
+Limited to 255 characters.
+
+=item Description
+
+Limited to 255 characters.
+
+=item Catalog
+
+=item Status
+
+=item Creator
+
+=item Created
+
+=item LastUpdatedBy
+
+=item LastUpdated
+
+=back
+
+All of these are readable through methods of the same name and mutable through
+methods of the same name with C<Set> prefixed. The last four are automatically
+managed.
+
+=head1 METHODS
+
+=head2 Load ID or NAME
+
+Loads the specified Asset into the current object.
+
+=cut
+
+sub Load {
+ my $self = shift;
+ my $id = shift;
+ return unless $id;
+
+ if ( $id =~ /\D/ ) {
+ return $self->LoadByCols( Name => $id );
+ }
+ else {
+ return $self->SUPER::Load($id);
+ }
+}
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database. Available keys are:
+
+=over 4
+
+=item Name
+
+=item Description
+
+=item Catalog
+
+Name or numeric ID
+
+=item CustomField-<ID>
+
+Sets the value for this asset of the custom field specified by C<< <ID> >>.
+
+C<< <ID> >> should be a numeric ID, but may also be a Name if and only if your
+custom fields have unique names. Without unique names, the behaviour is
+undefined.
+
+=item Status
+
+=item Owner, HeldBy, Contact
+
+A single principal ID or array ref of principal IDs to add as members of the
+respective role groups for the new asset.
+
+User Names and EmailAddresses may also be used, but Groups must be referenced
+by ID.
+
+=item RefersTo, ReferredToBy, DependsOn, DependedOnBy, Parents, Children, and aliases
+
+Any of these link types accept either a single value or arrayref of values
+parseable by L<RT::URI>.
+
+=back
+
+Returns a tuple of (status, msg) on failure and (id, msg, non-fatal errors) on
+success, where the third value is an array reference of errors that occurred
+but didn't prevent creation.
+
+=cut
+
+sub Create {
+ my $self = shift;
+ my %args = (
+ Name => '',
+ Description => '',
+ Catalog => undef,
+
+ Owner => undef,
+ HeldBy => undef,
+ Contact => undef,
+
+ Status => undef,
+ @_
+ );
+ my @non_fatal_errors;
+
+ return (0, $self->loc("Invalid Catalog"))
+ unless $self->ValidateCatalog( $args{'Catalog'} );
+
+ my $catalog = RT::Catalog->new( $self->CurrentUser );
+ $catalog->Load($args{'Catalog'});
+
+ $args{'Catalog'} = $catalog->id;
+
+ return (0, $self->loc("Permission Denied"))
+ unless $catalog->CurrentUserHasRight('CreateAsset');
+
+ return (0, $self->loc('Invalid Name (names may not be all digits)'))
+ unless $self->ValidateName( $args{'Name'} );
+
+ # XXX TODO: This status/lifecycle pattern is duplicated in RT::Ticket and
+ # should be refactored into a role helper.
+ my $cycle = $catalog->LifecycleObj;
+ unless ( defined $args{'Status'} && length $args{'Status'} ) {
+ $args{'Status'} = $cycle->DefaultOnCreate;
+ }
+
+ $args{'Status'} = lc $args{'Status'};
+ unless ( $cycle->IsValid( $args{'Status'} ) ) {
+ return ( 0,
+ $self->loc("Status '[_1]' isn't a valid status for assets.",
+ $self->loc($args{'Status'}))
+ );
+ }
+
+ unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
+ return ( 0,
+ $self->loc("New assets cannot have status '[_1]'.",
+ $self->loc($args{'Status'}))
+ );
+ }
+
+ my $roles = {};
+ my @errors = $self->_ResolveRoles( $roles, %args );
+ return (0, @errors) if @errors;
+
+ RT->DatabaseHandle->BeginTransaction();
+
+ my ( $id, $msg ) = $self->SUPER::Create(
+ map { $_ => $args{$_} } grep {exists $args{$_}}
+ qw(id Name Description Catalog Status),
+ );
+ unless ($id) {
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc("Asset create failed: [_1]", $msg));
+ }
+
+ # Let users who just created an asset see it until the end of this method.
+ $self->{_object_is_readable} = 1;
+
+ # Create role groups
+ unless ($self->_CreateRoleGroups()) {
+ RT->Logger->error("Couldn't create role groups for asset ". $self->id);
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc("Couldn't create role groups for asset"));
+ }
+
+ # Figure out users for roles
+ push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, map { $_ => sub {1} } $self->Roles );
+
+ # Add CFs
+ foreach my $key (keys %args) {
+ next unless $key =~ /^CustomField-(.+)$/i;
+ my $cf = $1;
+ my @vals = ref $args{$key} eq 'ARRAY' ? @{ $args{$key} } : $args{$key};
+ foreach my $value (@vals) {
+ next unless defined $value;
+
+ my ( $cfid, $cfmsg ) = $self->AddCustomFieldValue(
+ (ref($value) eq 'HASH'
+ ? %$value
+ : (Value => $value)),
+ Field => $cf,
+ RecordTransaction => 0
+ );
+ unless ($cfid) {
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc("Couldn't add custom field value on create: [_1]", $cfmsg));
+ }
+ }
+ }
+
+ # Create transaction
+ my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
+ unless ($txn_id) {
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc( 'Asset Create txn failed: [_1]', $txn_msg ));
+ }
+
+ # Add links
+ push @non_fatal_errors, $self->_AddLinksOnCreate(\%args);
+
+ RT->DatabaseHandle->Commit();
+
+ # Let normal ACLs take over.
+ delete $self->{_object_is_readable};
+
+ return ($id, $self->loc('Asset #[_1] created: [_2]', $self->id, $args{'Name'}), \@non_fatal_errors);
+}
+
+=head2 ValidateName NAME
+
+Requires that Names contain at least one non-digit. Empty names are OK.
+
+=cut
+
+sub ValidateName {
+ my $self = shift;
+ my $name = shift;
+ return 1 unless defined $name and length $name;
+ return 0 unless $name =~ /\D/;
+ return 1;
+}
+
+=head2 ValidateCatalog
+
+Takes a catalog name or ID. Returns true if the catalog exists and is not
+disabled, otherwise false.
+
+=cut
+
+sub ValidateCatalog {
+ my $self = shift;
+ my $name = shift;
+ my $catalog = RT::Catalog->new( $self->CurrentUser );
+ $catalog->Load($name);
+ return 1 if $catalog->id and not $catalog->Disabled;
+ return 0;
+}
+
+=head2 Delete
+
+Assets may not be deleted. Always returns failure.
+
+You should disable the asset instead with C<< $asset->SetStatus('deleted') >>.
+
+=cut
+
+sub Delete {
+ my $self = shift;
+ return (0, $self->loc("Assets may not be deleted"));
+}
+
+=head2 CurrentUserHasRight RIGHTNAME
+
+Returns true if the current user has the right for this asset, or globally if
+this is called on an unloaded object.
+
+=cut
+
+sub CurrentUserHasRight {
+ my $self = shift;
+ my $right = shift;
+
+ return (
+ $self->CurrentUser->HasRight(
+ Right => $right,
+ Object => ($self->id ? $self : RT->System),
+ )
+ );
+}
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the asset, either because they just
+created it or they have the I<ShowAsset> right.
+
+=cut
+
+sub CurrentUserCanSee {
+ my $self = shift;
+ return $self->{_object_is_readable} || $self->CurrentUserHasRight('ShowAsset');
+}
+
+=head2 URI
+
+Returns this asset's URI
+
+=cut
+
+sub URI {
+ my $self = shift;
+ my $uri = RT::URI::asset->new($self->CurrentUser);
+ return $uri->URIForObject($self);
+}
+
+=head2 CatalogObj
+
+Returns the L<RT::Catalog> object for this asset's catalog.
+
+=cut
+
+sub CatalogObj {
+ my $self = shift;
+ my $catalog = RT::Catalog->new($self->CurrentUser);
+ $catalog->Load( $self->__Value("Catalog") );
+ return $catalog;
+}
+
+=head2 SetCatalog
+
+Validates the supplied catalog and updates the column if valid. Transitions
+Status if necessary. Returns a (status, message) tuple.
+
+=cut
+
+sub SetCatalog {
+ my $self = shift;
+ my $value = shift;
+
+ return (0, $self->loc("Permission Denied"))
+ unless $self->CurrentUserHasRight("ModifyAsset");
+
+ my ($ok, $msg, $status) = $self->_SetLifecycleColumn(
+ Value => $value,
+ RequireRight => "CreateAsset"
+ );
+ return ($ok, $msg);
+}
+
+
+=head2 Owner
+
+Returns an L<RT::User> object for this asset's I<Owner> role group. On error,
+returns undef.
+
+=head2 HeldBy
+
+Returns an L<RT::Group> object for this asset's I<HeldBy> role group. The object
+may be unloaded if permissions aren't satisfied.
+
+=head2 Contacts
+
+Returns an L<RT::Group> object for this asset's I<Contact> role
+group. The object may be unloaded if permissions aren't satisfied.
+
+=cut
+
+sub Owner {
+ my $self = shift;
+ my $group = $self->RoleGroup("Owner");
+ return unless $group and $group->id;
+ return $group->UserMembersObj->First;
+}
+sub HeldBy { $_[0]->RoleGroup("HeldBy") }
+sub Contacts { $_[0]->RoleGroup("Contact") }
+
+=head2 AddRoleMember
+
+Checks I<ModifyAsset> before calling L<RT::Record::Role::Roles/_AddRoleMember>.
+
+=cut
+
+sub AddRoleMember {
+ my $self = shift;
+
+ return (0, $self->loc("No permission to modify this asset"))
+ unless $self->CurrentUserHasRight("ModifyAsset");
+
+ return $self->_AddRoleMember(@_);
+}
+
+=head2 DeleteRoleMember
+
+Checks I<ModifyAsset> before calling L<RT::Record::Role::Roles/_DeleteRoleMember>.
+
+=cut
+
+sub DeleteRoleMember {
+ my $self = shift;
+
+ return (0, $self->loc("No permission to modify this asset"))
+ unless $self->CurrentUserHasRight("ModifyAsset");
+
+ return $self->_DeleteRoleMember(@_);
+}
+
+=head2 RoleGroup
+
+An ACL'd version of L<RT::Record::Role::Roles/_RoleGroup>. Checks I<ShowAsset>.
+
+=cut
+
+sub RoleGroup {
+ my $self = shift;
+ if ($self->CurrentUserCanSee) {
+ return $self->_RoleGroup(@_);
+ } else {
+ return RT::Group->new( $self->CurrentUser );
+ }
+}
+
+=head1 INTERNAL METHODS
+
+Public methods, but you shouldn't need to call these unless you're
+extending Assets.
+
+=head2 CustomFieldLookupType
+
+=cut
+
+sub CustomFieldLookupType { "RT::Catalog-RT::Asset" }
+
+=head2 ACLEquivalenceObjects
+
+=cut
+
+sub ACLEquivalenceObjects {
+ my $self = shift;
+ return $self->CatalogObj;
+}
+
+=head2 ModifyLinkRight
+
+=cut
+
+# Used for StrictLinkACL and RT::Record::Role::Links.
+#
+# Historically StrictLinkACL has only applied between tickets, but
+# if you care about it enough to turn it on, you probably care when
+# linking an asset to an asset or an asset to a ticket.
+
+sub ModifyLinkRight { "ShowAsset" }
+
+=head2 LoadCustomFieldByIdentifier
+
+Finds and returns the custom field of the given name for the asset,
+overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
+catalog-specific CFs before global ones.
+
+=cut
+
+sub LoadCustomFieldByIdentifier {
+ my $self = shift;
+ my $field = shift;
+
+ return $self->SUPER::LoadCustomFieldByIdentifier($field)
+ if ref $field or $field =~ /^\d+$/;
+
+ my $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->SetContextObject( $self );
+ $cf->LoadByNameAndCatalog( Name => $field, Catalog => $self->Catalog );
+ $cf->LoadByNameAndCatalog( Name => $field, Catalog => 0 ) unless $cf->id;
+ return $cf;
+}
+
+=head1 PRIVATE METHODS
+
+Documented for internal use only, do not call these from outside RT::Asset
+itself.
+
+=head2 _Set
+
+Checks if the current user can I<ModifyAsset> before calling C<SUPER::_Set>
+and records a transaction against this object if C<SUPER::_Set> was
+successful.
+
+=cut
+
+sub _Set {
+ my $self = shift;
+ my %args = (
+ Field => undef,
+ Value => undef,
+ @_
+ );
+
+ return (0, $self->loc("Permission Denied"))
+ unless $self->CurrentUserHasRight('ModifyAsset');
+
+ my $old = $self->_Value( $args{'Field'} );
+
+ my ($ok, $msg) = $self->SUPER::_Set(@_);
+
+ # Only record the transaction if the _Set worked
+ return ($ok, $msg) unless $ok;
+
+ my $txn_type = $args{Field} eq "Status" ? "Status" : "Set";
+
+ my ($txn_id, $txn_msg, $txn) = $self->_NewTransaction(
+ Type => $txn_type,
+ Field => $args{'Field'},
+ NewValue => $args{'Value'},
+ OldValue => $old,
+ );
+
+ # Ensure that we can read the transaction, even if the change just made
+ # the asset unreadable to us. This is only in effect for the lifetime of
+ # $txn, i.e. as soon as this method returns.
+ $txn->{ _object_is_readable } = 1;
+
+ return ($txn_id, scalar $txn->BriefDescription);
+}
+
+=head2 _Value
+
+Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
+
+=cut
+
+sub _Value {
+ my $self = shift;
+ return unless $self->CurrentUserCanSee;
+ return $self->SUPER::_Value(@_);
+}
+
+sub Table { "Assets" }
+
+sub _CoreAccessible {
+ {
+ id => { read => 1, type => 'int(11)', default => '' },
+ Name => { read => 1, type => 'varchar(255)', default => '', write => 1 },
+ Status => { read => 1, type => 'varchar(64)', default => '', write => 1 },
+ Description => { read => 1, type => 'varchar(255)', default => '', write => 1 },
+ Catalog => { read => 1, type => 'int(11)', default => '0', write => 1 },
+ Creator => { read => 1, type => 'int(11)', default => '0', auto => 1 },
+ Created => { read => 1, type => 'datetime', default => '', auto => 1 },
+ LastUpdatedBy => { read => 1, type => 'int(11)', default => '0', auto => 1 },
+ LastUpdated => { read => 1, type => 'datetime', default => '', auto => 1 },
+ }
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/Assets.pm b/lib/RT/Assets.pm
new file mode 100644
index 0000000..67b1c45
--- /dev/null
+++ b/lib/RT/Assets.pm
@@ -0,0 +1,311 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Assets;
+use base 'RT::SearchBuilder';
+
+use Role::Basic "with";
+with "RT::SearchBuilder::Role::Roles" => { -rename => {RoleLimit => '_RoleLimit'}};
+
+use Scalar::Util qw/blessed/;
+
+=head1 NAME
+
+RT::Assets - a collection of L<RT::Asset> objects
+
+=head1 METHODS
+
+Only additional methods or overridden behaviour beyond the L<RT::SearchBuilder>
+(itself a L<DBIx::SearchBuilder>) class are documented below.
+
+=head2 LimitToActiveStatus
+
+=cut
+
+sub LimitToActiveStatus {
+ my $self = shift;
+
+ $self->Limit( FIELD => 'Status', VALUE => $_ )
+ for RT::Catalog->LifecycleObj->Valid('initial', 'active');
+}
+
+=head2 LimitCatalog
+
+Limit Catalog
+
+=cut
+
+sub LimitCatalog {
+ my $self = shift;
+ my %args = (
+ FIELD => 'Catalog',
+ OPERATOR => '=',
+ @_
+ );
+
+ if ( $args{OPERATOR} eq '=' ) {
+ $self->{Catalog} = $args{VALUE};
+ }
+ $self->SUPER::Limit(%args);
+}
+
+=head2 Limit
+
+Defaults CASESENSITIVE to 0
+
+=cut
+
+sub Limit {
+ my $self = shift;
+ my %args = (
+ CASESENSITIVE => 0,
+ @_
+ );
+ $self->SUPER::Limit(%args);
+}
+
+=head2 RoleLimit
+
+Re-uses the underlying JOIN, if possible.
+
+=cut
+
+sub RoleLimit {
+ my $self = shift;
+ my %args = (
+ TYPE => '',
+ SUBCLAUSE => '',
+ OPERATOR => '=',
+ @_
+ );
+
+ my $key = "role-join-".join("-",map {$args{$_}//''} qw/SUBCLAUSE TYPE OPERATOR/);
+ my @ret = $self->_RoleLimit(%args, BUNDLE => $self->{$key} );
+ $self->{$key} = \@ret;
+}
+
+=head1 INTERNAL METHODS
+
+Public methods which encapsulate implementation details. You shouldn't need to
+call these in normal code.
+
+=head2 AddRecord
+
+Checks the L<RT::Asset> is readable before adding it to the results
+
+=cut
+
+sub AddRecord {
+ my $self = shift;
+ my $asset = shift;
+ return unless $asset->CurrentUserCanSee;
+
+ return if $asset->__Value('Status') eq 'deleted'
+ and not $self->{'allow_deleted_search'};
+
+ $self->SUPER::AddRecord($asset, @_);
+}
+
+=head2 NewItem
+
+Returns a new empty RT::Asset item
+
+=cut
+
+sub NewItem {
+ my $self = shift;
+ return RT::Asset->new( $self->CurrentUser );
+}
+
+=head1 PRIVATE METHODS
+
+=head2 _Init
+
+Sets default ordering by Name ascending.
+
+=cut
+
+sub _Init {
+ my $self = shift;
+
+ $self->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
+ return $self->SUPER::_Init( @_ );
+}
+
+sub SimpleSearch {
+ my $self = shift;
+ my %args = (
+ Fields => RT->Config->Get('AssetSearchFields'),
+ Catalog => RT->Config->Get('DefaultCatalog'),
+ Term => undef,
+ @_
+ );
+
+ # XXX: We only search a single catalog so that we can map CF names
+ # to their ids, as searching CFs by CF name is rather complicated
+ # and currently fails in odd ways. Such a mapping obviously assumes
+ # that names are unique within the catalog, but ids are also
+ # allowable as well.
+ my $catalog;
+ if (ref $args{Catalog}) {
+ $catalog = $args{Catalog};
+ } else {
+ $catalog = RT::Catalog->new( $self->CurrentUser );
+ $catalog->Load( $args{Catalog} );
+ }
+
+ my %cfs;
+ my $cfs = $catalog->AssetCustomFields;
+ while (my $customfield = $cfs->Next) {
+ $cfs{$customfield->id} = $cfs{$customfield->Name}
+ = $customfield;
+ }
+
+ $self->LimitCatalog( VALUE => $catalog->id );
+
+ while (my ($name, $op) = each %{$args{Fields}}) {
+ $op = 'STARTSWITH'
+ unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
+ if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
+ my $cfname = $1 || $2;
+ $self->LimitCustomField(
+ CUSTOMFIELD => $cfs{$cfname},
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ ) if $cfs{$cfname};
+ } elsif ($name eq 'id' and $op =~ /(?:LIKE|(?:START|END)SWITH)$/i) {
+ $self->Limit(
+ FUNCTION => "CAST( main.$name AS TEXT )",
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ ) if $args{Term} =~ /^\d+$/;
+ } else {
+ $self->Limit(
+ FIELD => $name,
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ ) unless $args{Term} =~ /\D/ and $name eq 'id';
+ }
+ }
+ return $self;
+}
+
+sub OrderByCols {
+ my $self = shift;
+ my @res = ();
+
+ my $class = $self->_RoleGroupClass;
+
+ for my $row (@_) {
+ if ($row->{FIELD} =~ /^CF\.(?:\{(.*)\}|(.*))$/) {
+ my $name = $1 || $2;
+ my $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->LoadByNameAndCatalog(
+ Name => $name,
+ Catalog => $self->{'Catalog'},
+ );
+ if ( $cf->id ) {
+ push @res, $self->_OrderByCF( $row, $cf->id, $cf );
+ }
+ } elsif ($row->{FIELD} =~ /^(\w+)(?:\.(\w+))?$/) {
+ my ($role, $subkey) = ($1, $2);
+ if ($class->HasRole($role)) {
+ $self->{_order_by_role}{ $role }
+ ||= ( $self->_WatcherJoin( Name => $role, Class => $class) )[2];
+ push @res, {
+ %$row,
+ ALIAS => $self->{_order_by_role}{ $role },
+ FIELD => $subkey || 'EmailAddress',
+ };
+ } else {
+ push @res, $row;
+ }
+ } else {
+ push @res, $row;
+ }
+ }
+ return $self->SUPER::OrderByCols( @res );
+}
+
+=head2 _DoSearch
+
+=head2 _DoCount
+
+Limits to non-deleted assets unless the C<allow_deleted_search> flag is set.
+
+=cut
+
+sub _DoSearch {
+ my $self = shift;
+ $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+ unless $self->{'allow_deleted_search'};
+ $self->SUPER::_DoSearch(@_);
+}
+
+sub _DoCount {
+ my $self = shift;
+ $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+ unless $self->{'allow_deleted_search'};
+ $self->SUPER::_DoCount(@_);
+}
+
+sub Table { "Assets" }
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/Catalog.pm b/lib/RT/Catalog.pm
new file mode 100644
index 0000000..6da0a77
--- /dev/null
+++ b/lib/RT/Catalog.pm
@@ -0,0 +1,503 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Catalog;
+use base 'RT::Record';
+
+use Role::Basic 'with';
+with "RT::Record::Role::Lifecycle",
+ "RT::Record::Role::Roles" => {
+ -rename => {
+ # We provide ACL'd wraps of these.
+ AddRoleMember => "_AddRoleMember",
+ DeleteRoleMember => "_DeleteRoleMember",
+ RoleGroup => "_RoleGroup",
+ },
+ },
+ "RT::Record::Role::Rights";
+
+require RT::ACE;
+
+=head1 NAME
+
+RT::Catalog - A logical set of assets
+
+=cut
+
+# For the Lifecycle role
+sub LifecycleType { "asset" }
+
+# Setup rights
+__PACKAGE__->AddRight( General => ShowCatalog => 'See catalogs' ); #loc
+__PACKAGE__->AddRight( Admin => AdminCatalog => 'Create, modify, and disable catalogs' ); #loc
+
+__PACKAGE__->AddRight( General => ShowAsset => 'See assets' ); #loc
+__PACKAGE__->AddRight( Staff => CreateAsset => 'Create assets' ); #loc
+__PACKAGE__->AddRight( Staff => ModifyAsset => 'Modify assets' ); #loc
+
+__PACKAGE__->AddRight( General => SeeCustomField => 'View custom field values' ); # loc
+__PACKAGE__->AddRight( Staff => ModifyCustomField => 'Modify custom field values' ); # loc
+
+RT::ACE->RegisterCacheHandler(sub {
+ my %args = (
+ Action => "",
+ RightName => "",
+ @_
+ );
+
+ return unless $args{Action} =~ /^(Grant|Revoke)$/i
+ and $args{RightName} =~ /^(ShowCatalog|CreateAsset)$/;
+
+ RT::Catalog->CacheNeedsUpdate(1);
+});
+
+=head1 DESCRIPTION
+
+Catalogs are for assets what queues are for tickets or classes are for
+articles.
+
+It announces the rights for assets, and rights are granted at the catalog or
+global level. Asset custom fields are either applied globally to all Catalogs
+or individually to specific Catalogs.
+
+=over 4
+
+=item id
+
+=item Name
+
+Limited to 255 characters.
+
+=item Description
+
+Limited to 255 characters.
+
+=item Lifecycle
+
+=item Disabled
+
+=item Creator
+
+=item Created
+
+=item LastUpdatedBy
+
+=item LastUpdated
+
+=back
+
+All of these are readable through methods of the same name and mutable through
+methods of the same name with C<Set> prefixed. The last four are automatically
+managed.
+
+=head1 METHODS
+
+=head2 Load ID or NAME
+
+Loads the specified Catalog into the current object.
+
+=cut
+
+sub Load {
+ my $self = shift;
+ my $id = shift;
+ return unless $id;
+
+ if ( $id =~ /\D/ ) {
+ return $self->LoadByCols( Name => $id );
+ }
+ else {
+ return $self->SUPER::Load($id);
+ }
+}
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database. Available keys are:
+
+=over 4
+
+=item Name
+
+=item Description
+
+=item Lifecycle
+
+=item HeldBy, Contact
+
+A single principal ID or array ref of principal IDs to add as members of the
+respective role groups for the new catalog.
+
+User Names and EmailAddresses may also be used, but Groups must be referenced
+by ID.
+
+=item Disabled
+
+=back
+
+Returns a tuple of (status, msg) on failure and (id, msg, non-fatal errors) on
+success, where the third value is an array reference of errors that occurred
+but didn't prevent creation.
+
+=cut
+
+sub Create {
+ my $self = shift;
+ my %args = (
+ Name => '',
+ Description => '',
+ Lifecycle => 'assets',
+
+ HeldBy => undef,
+ Contact => undef,
+
+ Disabled => 0,
+
+ @_
+ );
+ my @non_fatal_errors;
+
+ return (0, $self->loc("Permission Denied"))
+ unless $self->CurrentUserHasRight('AdminCatalog');
+
+ return (0, $self->loc('Invalid Name (names must be unique and may not be all digits)'))
+ unless $self->ValidateName( $args{'Name'} );
+
+ $args{'Lifecycle'} ||= 'assets';
+
+ return (0, $self->loc('[_1] is not a valid lifecycle', $args{'Lifecycle'}))
+ unless $self->ValidateLifecycle( $args{'Lifecycle'} );
+
+ RT->DatabaseHandle->BeginTransaction();
+
+ my ( $id, $msg ) = $self->SUPER::Create(
+ map { $_ => $args{$_} } qw(Name Description Lifecycle Disabled),
+ );
+ unless ($id) {
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc("Catalog create failed: [_1]", $msg));
+ }
+
+ # Create role groups
+ unless ($self->_CreateRoleGroups()) {
+ RT->Logger->error("Couldn't create role groups for catalog ". $self->id);
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc("Couldn't create role groups for catalog"));
+ }
+
+ # Figure out users for roles
+ my $roles = {};
+ push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
+ push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, map { $_ => sub {1} } $self->Roles );
+
+ # Create transaction
+ my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
+ unless ($txn_id) {
+ RT->DatabaseHandle->Rollback();
+ return (0, $self->loc( 'Catalog Create txn failed: [_1]', $txn_msg ));
+ }
+
+ $self->CacheNeedsUpdate(1);
+ RT->DatabaseHandle->Commit();
+
+ return ($id, $self->loc('Catalog #[_1] created: [_2]', $self->id, $args{'Name'}), \@non_fatal_errors);
+}
+
+=head2 ValidateName NAME
+
+Requires that Names contain at least one non-digit and doesn't already exist.
+
+=cut
+
+sub ValidateName {
+ my $self = shift;
+ my $name = shift;
+ return 0 unless defined $name and length $name;
+ return 0 unless $name =~ /\D/;
+
+ my $catalog = RT::Catalog->new( RT->SystemUser );
+ $catalog->Load($name);
+ return 0 if $catalog->id;
+
+ return 1;
+}
+
+=head2 Delete
+
+Catalogs may not be deleted. Always returns failure.
+
+You should disable the catalog instead using C<< $catalog->SetDisabled(1) >>.
+
+=cut
+
+sub Delete {
+ my $self = shift;
+ return (0, $self->loc("Catalogs may not be deleted"));
+}
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the catalog via the I<ShowCatalog> or
+I<AdminCatalog> rights.
+
+=cut
+
+sub CurrentUserCanSee {
+ my $self = shift;
+ return $self->CurrentUserHasRight('ShowCatalog')
+ || $self->CurrentUserHasRight('AdminCatalog');
+}
+
+=head2 Owner
+
+Returns an L<RT::User> object for this catalog's I<Owner> role group. On error,
+returns undef.
+
+=head2 HeldBy
+
+Returns an L<RT::Group> object for this catalog's I<HeldBy> role group. The object
+may be unloaded if permissions aren't satisfied.
+
+=head2 Contacts
+
+Returns an L<RT::Group> object for this catalog's I<Contact> role
+group. The object may be unloaded if permissions aren't satisfied.
+
+=cut
+
+sub Owner {
+ my $self = shift;
+ my $group = $self->RoleGroup("Owner");
+ return unless $group and $group->id;
+ return $group->UserMembersObj->First;
+}
+sub HeldBy { $_[0]->RoleGroup("HeldBy") }
+sub Contacts { $_[0]->RoleGroup("Contact") }
+
+=head2 AddRoleMember
+
+Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_AddRoleMember>.
+
+=cut
+
+sub AddRoleMember {
+ my $self = shift;
+
+ return (0, $self->loc("No permission to modify this catalog"))
+ unless $self->CurrentUserHasRight("AdminCatalog");
+
+ return $self->_AddRoleMember(@_);
+}
+
+=head2 DeleteRoleMember
+
+Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_DeleteRoleMember>.
+
+=cut
+
+sub DeleteRoleMember {
+ my $self = shift;
+
+ return (0, $self->loc("No permission to modify this catalog"))
+ unless $self->CurrentUserHasRight("AdminCatalog");
+
+ return $self->_DeleteRoleMember(@_);
+}
+
+=head2 RoleGroup
+
+An ACL'd version of L<RT::Record::Role::Roles/_RoleGroup>. Checks I<ShowCatalog>.
+
+=cut
+
+sub RoleGroup {
+ my $self = shift;
+ if ($self->CurrentUserCanSee) {
+ return $self->_RoleGroup(@_);
+ } else {
+ return RT::Group->new( $self->CurrentUser );
+ }
+}
+
+=head2 AssetCustomFields
+
+Returns an L<RT::CustomFields> object containing all global and
+catalog-specific B<asset> custom fields.
+
+=cut
+
+sub AssetCustomFields {
+ my $self = shift;
+ my $cfs = RT::CustomFields->new( $self->CurrentUser );
+ if ($self->CurrentUserCanSee) {
+ $cfs->SetContextObject( $self );
+ $cfs->LimitToGlobalOrObjectId( $self->Id );
+ $cfs->LimitToLookupType( RT::Asset->CustomFieldLookupType );
+ $cfs->ApplySortOrder;
+ } else {
+ $cfs->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
+ }
+ return ($cfs);
+}
+
+=head1 INTERNAL METHODS
+
+=head2 CacheNeedsUpdate
+
+Takes zero or one arguments.
+
+If a true argument is provided, marks any Catalog caches as needing an update.
+This happens when catalogs are created, disabled/enabled, or modified. Returns
+nothing.
+
+If no arguments are provided, returns an epoch time that any catalog caches
+should be newer than.
+
+May be called as a class or object method.
+
+=cut
+
+sub CacheNeedsUpdate {
+ my $class = shift;
+ my $update = shift;
+
+ if ($update) {
+ RT->System->SetAttribute(Name => 'CatalogCacheNeedsUpdate', Content => time);
+ return;
+ } else {
+ my $attribute = RT->System->FirstAttribute('CatalogCacheNeedsUpdate');
+ return $attribute ? $attribute->Content : 0;
+ }
+}
+
+=head1 PRIVATE METHODS
+
+Documented for internal use only, do not call these from outside RT::Catalog
+itself.
+
+=head2 _Set
+
+Checks if the current user can I<AdminCatalog> before calling C<SUPER::_Set>
+and records a transaction against this object if C<SUPER::_Set> was
+successful.
+
+=cut
+
+sub _Set {
+ my $self = shift;
+ my %args = (
+ Field => undef,
+ Value => undef,
+ @_
+ );
+
+ return (0, $self->loc("Permission Denied"))
+ unless $self->CurrentUserHasRight('AdminCatalog');
+
+ my $old = $self->_Value( $args{'Field'} );
+
+ my ($ok, $msg) = $self->SUPER::_Set(@_);
+
+ # Only record the transaction if the _Set worked
+ return ($ok, $msg) unless $ok;
+
+ my $txn_type = "Set";
+ if ($args{'Field'} eq "Disabled") {
+ if (not $old and $args{'Value'}) {
+ $txn_type = "Disabled";
+ }
+ elsif ($old and not $args{'Value'}) {
+ $txn_type = "Enabled";
+ }
+ }
+
+ $self->CacheNeedsUpdate(1);
+
+ my ($txn_id, $txn_msg, $txn) = $self->_NewTransaction(
+ Type => $txn_type,
+ Field => $args{'Field'},
+ NewValue => $args{'Value'},
+ OldValue => $old,
+ );
+ return ($txn_id, scalar $txn->BriefDescription);
+}
+
+=head2 _Value
+
+Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
+
+=cut
+
+sub _Value {
+ my $self = shift;
+ return unless $self->CurrentUserCanSee;
+ return $self->SUPER::_Value(@_);
+}
+
+sub Table { "Catalogs" }
+
+sub _CoreAccessible {
+ {
+ id => { read => 1, type => 'int(11)', default => '' },
+ Name => { read => 1, type => 'varchar(255)', default => '', write => 1 },
+ Description => { read => 1, type => 'varchar(255)', default => '', write => 1 },
+ Lifecycle => { read => 1, type => 'varchar(32)', default => 'assets', write => 1 },
+ Disabled => { read => 1, type => 'int(2)', default => '0', write => 1 },
+ Creator => { read => 1, type => 'int(11)', default => '0', auto => 1 },
+ Created => { read => 1, type => 'datetime', default => '', auto => 1 },
+ LastUpdatedBy => { read => 1, type => 'int(11)', default => '0', auto => 1 },
+ LastUpdated => { read => 1, type => 'datetime', default => '', auto => 1 },
+ }
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/Catalogs.pm b/lib/RT/Catalogs.pm
new file mode 100644
index 0000000..6ee6ee9
--- /dev/null
+++ b/lib/RT/Catalogs.pm
@@ -0,0 +1,130 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Catalogs;
+use base 'RT::SearchBuilder';
+
+=head1 NAME
+
+RT::Catalogs - a collection of L<RT::Catalog> objects
+
+=head1 METHODS
+
+Only additional methods or overridden behaviour beyond the L<RT::SearchBuilder>
+(itself a L<DBIx::SearchBuilder>) class are documented below.
+
+=head2 Limit
+
+Defaults CASESENSITIVE to 0
+
+=cut
+
+sub Limit {
+ my $self = shift;
+ my %args = (
+ CASESENSITIVE => 0,
+ @_
+ );
+ $self->SUPER::Limit(%args);
+}
+
+=head1 INTERNAL METHODS
+
+Public methods which encapsulate implementation details. You shouldn't need to
+call these in normal code.
+
+=head2 AddRecord
+
+Checks the L<RT::Catalog> is readable before adding it to the results
+
+=cut
+
+sub AddRecord {
+ my $self = shift;
+ my $catalog = shift;
+ return unless $catalog->CurrentUserCanSee;
+
+ $self->SUPER::AddRecord($catalog, @_);
+}
+
+=head2 NewItem
+
+Returns a new empty RT::Catalog item
+
+=cut
+
+sub NewItem {
+ my $self = shift;
+ return RT::Catalog->new( $self->CurrentUser );
+}
+
+=head1 PRIVATE METHODS
+
+=head2 _Init
+
+Sets default ordering by Name ascending.
+
+=cut
+
+sub _Init {
+ my $self = shift;
+
+ $self->{'with_disabled_column'} = 1;
+
+ $self->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
+ return $self->SUPER::_Init( @_ );
+}
+
+sub Table { "Catalogs" }
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 0603927..9627b1b 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -2331,6 +2331,91 @@ sub __DependsOn {
return $self->SUPER::__DependsOn( %args );
}
+=head2 LoadByNameAndCatalog
+
+Loads the described asset custom field, if one is found, into the current
+object. This method only consults custom fields applied to L<RT::Catalog> for
+L<RT::Asset> objects.
+
+Takes a hash with the keys:
+
+=over
+
+=item Name
+
+A L<RT::CustomField> ID or Name which applies to L<assets|RT::Asset>.
+
+=item Catalog
+
+Optional. An L<RT::Catalog> ID or Name.
+
+=back
+
+If Catalog is specified, only a custom field added to that Catalog will be loaded.
+
+If Catalog is C<0>, only global asset custom fields will be loaded.
+
+If no Catalog is specified, all asset custom fields are searched including
+global and catalog-specific CFs.
+
+Please note that this method may load a Disabled custom field if no others
+matching the same criteria are found. Enabled CFs are preferentially loaded.
+
+=cut
+
+# To someday be merged into RT::CustomField::LoadByName
+sub LoadByNameAndCatalog {
+ my $self = shift;
+ my %args = (
+ Catalog => undef,
+ Name => undef,
+ @_,
+ );
+
+ unless ( defined $args{'Name'} && length $args{'Name'} ) {
+ $RT::Logger->error("Couldn't load Custom Field without Name");
+ return wantarray ? (0, $self->loc("No name provided")) : 0;
+ }
+
+ # if we're looking for a catalog by name, make it a number
+ if ( defined $args{'Catalog'} && ($args{'Catalog'} =~ /\D/ || !$self->ContextObject) ) {
+ my $CatalogObj = RT::Catalog->new( $self->CurrentUser );
+ my ($ok, $msg) = $CatalogObj->Load( $args{'Catalog'} );
+ if ( $ok ){
+ $args{'Catalog'} = $CatalogObj->Id;
+ }
+ elsif ($args{'Catalog'}) {
+ RT::Logger->error("Unable to load catalog " . $args{'Catalog'} . $msg);
+ return (0, $msg);
+ }
+ $self->SetContextObject( $CatalogObj )
+ unless $self->ContextObject;
+ }
+
+ my $CFs = RT::CustomFields->new( $self->CurrentUser );
+ $CFs->SetContextObject( $self->ContextObject );
+ my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
+ $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
+
+ # Limit to catalog, if provided. This will also limit to RT::Asset types.
+ $CFs->LimitToCatalog( $args{'Catalog'} );
+
+ # When loading by name, we _can_ load disabled fields, but prefer
+ # non-disabled fields.
+ $CFs->FindAllRows;
+ $CFs->OrderByCols(
+ {
+ FIELD => "Disabled", ORDER => 'ASC' },
+ );
+
+ # We only want one entry.
+ $CFs->RowsPerPage(1);
+
+ return (0, $self->loc("Not found")) unless my $first = $CFs->First;
+ return $self->LoadById( $first->id );
+}
+
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/CustomFields.pm b/lib/RT/CustomFields.pm
index a93bfc8..7b86390 100644
--- a/lib/RT/CustomFields.pm
+++ b/lib/RT/CustomFields.pm
@@ -409,6 +409,37 @@ sub NewItem {
return $res;
}
+=head2 LimitToCatalog
+
+Takes a numeric L<RT::Catalog> ID. Limits the L<RT::CustomFields> collection
+to only those fields applied directly to the specified catalog. This limit is
+OR'd with other L</LimitToCatalog> and L<RT::CustomFields/LimitToObjectId>
+calls.
+
+Note that this will cause the collection to only return asset CFs.
+
+=cut
+
+sub LimitToCatalog {
+ my $self = shift;
+ my $catalog = shift;
+
+ $self->Limit (ALIAS => $self->_OCFAlias,
+ ENTRYAGGREGATOR => 'OR',
+ FIELD => 'ObjectId',
+ VALUE => "$catalog")
+ if defined $catalog;
+
+ $self->LimitToLookupType( RT::Asset->CustomFieldLookupType );
+ $self->ApplySortOrder;
+
+ unless ($self->ContextObject) {
+ my $obj = RT::Catalog->new( $self->CurrentUser );
+ $obj->Load( $catalog );
+ $self->SetContextObject( $obj );
+ }
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 413c165..f5c2f91 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -127,6 +127,7 @@ sub JSFiles {
forms.js
event-registration.js
late.js
+ assets.js
/static/RichText/ckeditor.js
}, RT->Config->Get('JSFiles');
}
@@ -3868,6 +3869,250 @@ sub GetPrincipalsMap {
return @map;
}
+sub LoadCatalog {
+ my $id = shift
+ or Abort(loc("No catalog specified."));
+
+ my $catalog = RT::Catalog->new( $session{CurrentUser} );
+ $catalog->Load($id);
+
+ Abort(loc("Unable to find catalog [_1]", $id))
+ unless $catalog->id;
+
+ Abort(loc("You don't have permission to view this catalog."))
+ unless $catalog->CurrentUserCanSee;
+
+ return $catalog;
+}
+
+sub LoadAsset {
+ my $id = shift
+ or Abort(loc("No asset ID specified."));
+
+ my $asset = RT::Asset->new( $session{CurrentUser} );
+ $asset->Load($id);
+
+ Abort(loc("Unable to find asset #[_1]", $id))
+ unless $asset->id;
+
+ Abort(loc("You don't have permission to view this asset."))
+ unless $asset->CurrentUserCanSee;
+
+ return $asset;
+}
+
+sub ProcessRoleMembers {
+ my $object = shift;
+ my %ARGS = (@_);
+ my @results;
+
+ for my $arg (keys %ARGS) {
+ if ($arg =~ /^Add(User|Group)RoleMember$/) {
+ next unless $ARGS{$arg} and $ARGS{"$arg-Role"};
+
+ my ($ok, $msg) = $object->AddRoleMember(
+ Type => $ARGS{"$arg-Role"},
+ $1 => $ARGS{$arg},
+ );
+ push @results, $msg;
+ }
+ elsif ($arg =~ /^SetRoleMember-(.+)$/) {
+ my $role = $1;
+ my $group = $object->RoleGroup($role);
+ next unless $group->id and $group->SingleMemberRoleGroup;
+ next if $ARGS{$arg} eq $group->UserMembersObj->First->Name;
+ my ($ok, $msg) = $object->AddRoleMember(
+ Type => $role,
+ User => $ARGS{$arg} || 'Nobody',
+ );
+ push @results, $msg;
+ }
+ elsif ($arg =~ /^(Add|Remove)RoleMember-(.+)$/) {
+ my $role = $2;
+ my $method = $1 eq 'Add'? 'AddRoleMember' : 'DeleteRoleMember';
+
+ my $is = 'User';
+ if ( ($ARGS{"$arg-Type"}||'') =~ /^(User|Group)$/ ) {
+ $is = $1;
+ }
+
+ my ($ok, $msg) = $object->$method(
+ Type => $role,
+ ($ARGS{$arg} =~ /\D/
+ ? ($is => $ARGS{$arg})
+ : (PrincipalId => $ARGS{$arg})
+ ),
+ );
+ push @results, $msg;
+ }
+ elsif ($arg =~ /^RemoveAllRoleMembers-(.+)$/) {
+ my $role = $1;
+ my $group = $object->RoleGroup($role);
+ next unless $group->id;
+
+ my $gms = $group->MembersObj;
+ while ( my $gm = $gms->Next ) {
+ my ($ok, $msg) = $object->DeleteRoleMember(
+ Type => $role,
+ PrincipalId => $gm->MemberId,
+ );
+ push @results, $msg;
+ }
+ }
+ }
+ return @results;
+}
+
+
+# If provided a catalog, load it and return the object.
+# If no catalog is passed, load the first active catalog.
+
+sub LoadDefaultCatalog {
+ my $catalog = shift;
+ my $catalog_obj = RT::Catalog->new($session{CurrentUser});
+
+ if ( $catalog ){
+ $catalog_obj->Load($catalog);
+ RT::Logger->error("Unable to load catalog: " . $catalog)
+ unless $catalog_obj->Id;
+ }
+ elsif ( $session{'DefaultCatalog'} ){
+ $catalog_obj->Load($session{'DefaultCatalog'});
+ RT::Logger->error("Unable to load remembered catalog: " .
+ $session{'DefaultCatalog'})
+ unless $catalog_obj->Id;
+ }
+ elsif ( RT->Config->Get("DefaultCatalog") ){
+ $catalog_obj->Load( RT->Config->Get("DefaultCatalog") );
+ RT::Logger->error("Unable to load default catalog: "
+ . RT->Config->Get("DefaultCatalog"))
+ unless $catalog_obj->Id;
+ }
+ else {
+ # If no catalog, default to the first active catalog
+ my $catalogs = RT::Catalogs->new($session{CurrentUser});
+ $catalogs->UnLimit;
+ $catalog_obj = $catalogs->First();
+ RT::Logger->error("No active catalogs.")
+ unless $catalog_obj and $catalog_obj->Id;
+ }
+
+ return $catalog_obj;
+}
+
+sub ProcessAssetsSearchArguments {
+ my %args = (
+ Catalog => undef,
+ Assets => undef,
+ ARGSRef => undef,
+ @_
+ );
+ my $ARGSRef = $args{'ARGSRef'};
+
+ my @PassArguments;
+
+ if ($ARGSRef->{q}) {
+ if ($ARGSRef->{q} =~ /^\d+$/) {
+ my $asset = RT::Asset->new( $session{CurrentUser} );
+ $asset->Load( $ARGSRef->{q} );
+ RT::Interface::Web::Redirect(
+ RT->Config->Get('WebURL')."Asset/Display.html?id=".$ARGSRef->{q}
+ ) if $asset->id;
+ }
+ $args{'Assets'}->SimpleSearch( Term => $ARGSRef->{q}, Catalog => $args{Catalog} );
+ push @PassArguments, "q";
+ } elsif ( $ARGSRef->{'SearchAssets'} ){
+ for my $key (keys %$ARGSRef) {
+ my $value = ref $ARGSRef->{$key} ? $ARGSRef->{$key}[0] : $ARGSRef->{$key};
+ next unless defined $value and length $value;
+
+ my $orig_key = $key;
+ my $negative = ($key =~ s/^!// ? 1 : 0);
+ if ($key =~ /^(Name|Description)$/) {
+ $args{'Assets'}->Limit(
+ FIELD => $key,
+ OPERATOR => ($negative ? 'NOT LIKE' : 'LIKE'),
+ VALUE => $value,
+ ENTRYAGGREGATOR => "AND",
+ );
+ } elsif ($key eq 'Catalog') {
+ $args{'Assets'}->LimitCatalog(
+ OPERATOR => ($negative ? '!=' : '='),
+ VALUE => $value,
+ ENTRYAGGREGATOR => "AND",
+ );
+ } elsif ($key eq 'Status') {
+ $args{'Assets'}->Limit(
+ FIELD => $key,
+ OPERATOR => ($negative ? '!=' : '='),
+ VALUE => $value,
+ ENTRYAGGREGATOR => "AND",
+ );
+ } elsif ($key =~ /^Role\.(.+)/) {
+ my $role = $1;
+ $args{'Assets'}->RoleLimit(
+ TYPE => $role,
+ FIELD => $_,
+ OPERATOR => ($negative ? '!=' : '='),
+ VALUE => $value,
+ SUBCLAUSE => $role,
+ ENTRYAGGREGATOR => ($negative ? "AND" : "OR"),
+ CASESENSITIVE => 0,
+ ) for qw/EmailAddress Name/;
+ } elsif ($key =~ /^CF\.\{(.+?)\}$/ or $key =~ /^CF\.(.*)/) {
+ my $cf = RT::Asset->new( $session{CurrentUser} )
+ ->LoadCustomFieldByIdentifier( $1 );
+ next unless $cf->id;
+ if ( $value eq 'NULL' ) {
+ $args{'Assets'}->LimitCustomField(
+ CUSTOMFIELD => $cf->Id,
+ OPERATOR => ($negative ? "IS NOT" : "IS"),
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => "AND",
+ );
+ } else {
+ $args{'Assets'}->LimitCustomField(
+ CUSTOMFIELD => $cf->Id,
+ OPERATOR => ($negative ? "NOT LIKE" : "LIKE"),
+ VALUE => $value,
+ ENTRYAGGREGATOR => "AND",
+ );
+ }
+ }
+ else {
+ next;
+ }
+ push @PassArguments, $orig_key;
+ }
+ push @PassArguments, 'SearchAssets';
+ }
+
+ my $Format = RT->Config->Get('AssetSearchFormat');
+ $Format = $Format->{$args{'Catalog'}->id}
+ || $Format->{$args{'Catalog'}->Name}
+ || $Format->{''} if ref $Format;
+ $Format ||= q[
+ '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
+ '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
+ Description,
+ Status,
+ ];
+
+ $ARGSRef->{OrderBy} ||= 'id';
+
+ push @PassArguments, qw/OrderBy Order Page/;
+
+ return (
+ OrderBy => 'id',
+ Order => 'ASC',
+ Rows => 50,
+ (map { $_ => $ARGSRef->{$_} } grep { defined $ARGSRef->{$_} } @PassArguments),
+ PassArguments => \@PassArguments,
+ Format => $Format,
+ );
+}
+
=head2 _load_container_object ( $type, $id );
Instantiate container object for saving searches.
diff --git a/lib/RT/Lifecycle/Asset.pm b/lib/RT/Lifecycle/Asset.pm
new file mode 100644
index 0000000..88f0c6b
--- /dev/null
+++ b/lib/RT/Lifecycle/Asset.pm
@@ -0,0 +1,92 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Lifecycle::Asset;
+__PACKAGE__->RegisterRights;
+
+use base qw(RT::Lifecycle);
+
+=head2 Catalogs
+
+Returns L<RT::Catalogs> collection with catalogs that use this lifecycle.
+
+=cut
+
+sub Catalogs {
+ my $self = shift;
+ require RT::Catalogs;
+ my $catalogs = RT::Catalogs->new( RT->SystemUser );
+ $catalogs->Limit( FIELD => 'Lifecycle', VALUE => $self->Name );
+ return $catalogs;
+}
+
+=head2 RegisterRights
+
+Asset lifecycle rights are registered (and thus grantable) at the
+catalog level.
+
+=cut
+
+sub RegisterRights {
+ my $self = shift;
+
+ my %rights = $self->RightsDescription( 'asset' );
+
+ require RT::ACE;
+
+ while ( my ($right, $description) = each %rights ) {
+ next if RT::ACE->CanonicalizeRightName( $right );
+
+ RT::Catalog->AddRight( Status => $right => $description );
+ }
+}
+
+1;
diff --git a/lib/RT/Test/Assets.pm b/lib/RT/Test/Assets.pm
new file mode 100644
index 0000000..75af2fd
--- /dev/null
+++ b/lib/RT/Test/Assets.pm
@@ -0,0 +1,83 @@
+use strict;
+use warnings;
+
+package RT::Test::Assets;
+use base 'RT::Test';
+
+our @EXPORT = qw(create_catalog create_asset create_assets create_cf apply_cfs);
+
+sub import {
+ my $class = shift;
+ my %args = @_;
+
+ $class->SUPER::import( %args );
+ __PACKAGE__->export_to_level(1);
+}
+
+sub diag {
+ Test::More::diag(@_) if $ENV{TEST_VERBOSE};
+}
+
+sub create_catalog {
+ my %info = @_;
+ my $catalog = RT::Catalog->new( RT->SystemUser );
+ my ($id, $msg) = $catalog->Create( %info );
+ if ($id) {
+ diag("Created catalog #$id: " . $catalog->Name);
+ return $catalog;
+ } else {
+ my $spec = join "/", map { "$_=$info{$_}" } keys %info;
+ RT->Logger->error("Failed to create catalog ($spec): $msg");
+ return;
+ }
+}
+
+sub create_asset {
+ my %info = @_;
+ my $asset = RT::Asset->new( RT->SystemUser );
+ my ($id, $msg) = $asset->Create( %info );
+ if ($id) {
+ diag("Created asset #$id: " . $asset->Name);
+ return $asset;
+ } else {
+ my $spec = join "/", map { "$_=$info{$_}" } keys %info;
+ RT->Logger->error("Failed to create asset ($spec): $msg");
+ return;
+ }
+}
+
+sub create_assets {
+ my $error = 0;
+ for my $info (@_) {
+ create_asset(%$info)
+ or $error++;
+ }
+ return not $error;
+}
+
+sub create_cf {
+ my %args = (
+ Name => "Test Asset CF ".($$ + rand(1024)),
+ Type => "FreeformSingle",
+ LookupType => RT::Asset->CustomFieldLookupType,
+ @_,
+ );
+ my $cf = RT::CustomField->new( RT->SystemUser );
+ my ($ok, $msg) = $cf->Create(%args);
+ RT->Logger->error("Can't create CF: $msg") unless $ok;
+ return $cf;
+}
+
+sub apply_cfs {
+ my $success = 1;
+ for my $cf (@_) {
+ my ($ok, $msg) = $cf->AddToObject( RT::Catalog->new(RT->SystemUser) );
+ if (not $ok) {
+ RT->Logger->error("Couldn't apply CF: $msg");
+ $success = 0;
+ }
+ }
+ return $success;
+}
+
+1;
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index d909171..b4a4a01 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -1285,7 +1285,16 @@ sub _FormatUser {
"#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
];
return ("Reminder '[_1]' completed", $subject); #loc()
- }
+ },
+ 'RT::Asset-Set-Catalog' => sub {
+ my $self = shift;
+ return ("[_1] changed from [_2] to [_3]", #loc
+ $self->loc($self->Field), map {
+ my $c = RT::Catalog->new($self->CurrentUser);
+ $c->Load($_);
+ $c->Name || $self->loc("~[a hidden catalog~]")
+ } $self->OldValue, $self->NewValue);
+ },
);
diff --git a/lib/RT/URI/asset.pm b/lib/RT/URI/asset.pm
new file mode 100644
index 0000000..aeab115
--- /dev/null
+++ b/lib/RT/URI/asset.pm
@@ -0,0 +1,212 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::URI::asset;
+use base qw/RT::URI::base/;
+
+require RT::Asset;
+
+=head1 NAME
+
+RT::URI::asset - Internal URIs for linking to an L<RT::Asset>
+
+=head1 DESCRIPTION
+
+This class should rarely be used directly, but via L<RT::URI> instead.
+
+Represents, parses, and generates internal RT URIs such as:
+
+ asset:42
+ asset://example.com/42
+
+These URIs are used to link between objects in RT such as associating an asset
+with a ticket or an asset with another asset.
+
+=head1 METHODS
+
+Much of the interface below is dictated by L<RT::URI> and L<RT::URI::base>.
+
+=head2 Scheme
+
+Return the URI scheme for assets
+
+=cut
+
+sub Scheme { "asset" }
+
+=head2 LocalURIPrefix
+
+Returns the site-specific prefix for a local asset URI
+
+=cut
+
+sub LocalURIPrefix {
+ my $self = shift;
+ return $self->Scheme . "://" . RT->Config->Get('Organization') . "/";
+}
+
+=head2 IsLocal
+
+Returns a true value, the asset ID, if this object represents a local asset,
+undef otherwise.
+
+=cut
+
+sub IsLocal {
+ my $self = shift;
+ my $prefix = $self->LocalURIPrefix;
+ return $1 if $self->{uri} =~ /^\Q$prefix\E(\d+)/i;
+ return undef;
+}
+
+=head2 URIForObject RT::Asset
+
+Returns the URI for a local L<RT::Asset> object
+
+=cut
+
+sub URIForObject {
+ my $self = shift;
+ my $obj = shift;
+ return $self->LocalURIPrefix . $obj->Id;
+}
+
+=head2 ParseURI URI
+
+Primarily used by L<RT::URI> to set internal state.
+
+Figures out from an C<asset:> URI whether it refers to a local asset and the
+asset ID.
+
+Returns the asset ID if local, otherwise returns false.
+
+=cut
+
+sub ParseURI {
+ my $self = shift;
+ my $uri = shift;
+
+ my $scheme = $self->Scheme;
+
+ # canonicalize "42" and "asset:42" -> asset://example.com/42
+ if ($uri =~ /^(?:\Q$scheme\E:)?(\d+)$/i) {
+ $self->{'uri'} = $self->LocalURIPrefix . $1;
+ }
+ else {
+ $self->{'uri'} = $uri;
+ }
+
+ my $asset = RT::Asset->new( $self->CurrentUser );
+ if ( my $id = $self->IsLocal ) {
+ $asset->Load($id);
+
+ if ($asset->id) {
+ $self->{'object'} = $asset;
+ } else {
+ RT->Logger->error("Can't load Asset #$id by URI '$uri'");
+ return;
+ }
+ }
+ return $asset->id;
+}
+
+=head2 Object
+
+Returns the object for this URI, if it's local. Otherwise returns undef.
+
+=cut
+
+sub Object {
+ my $self = shift;
+ return $self->{'object'};
+}
+
+=head2 HREF
+
+If this is a local asset, return an HTTP URL for it.
+
+Otherwise, return its URI.
+
+=cut
+
+sub HREF {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ return RT->Config->Get('WebURL')
+ . ( $self->CurrentUser->Privileged ? "" : "SelfService/" )
+ . "Asset/Display.html?id="
+ . $self->Object->Id;
+ } else {
+ return $self->URI;
+ }
+}
+
+=head2 AsString
+
+Returns a description of this object
+
+=cut
+
+sub AsString {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ my $object = $self->Object;
+ if ( $object->Name ) {
+ return $self->loc('Asset #[_1]: [_2]', $object->id, $object->Name);
+ } else {
+ return $self->loc('Asset #[_1]', $object->id);
+ }
+ } else {
+ return $self->SUPER::AsString(@_);
+ }
+}
+
+1;
diff --git a/share/html/Admin/Assets/Catalogs/Create.html b/share/html/Admin/Assets/Catalogs/Create.html
new file mode 100644
index 0000000..1179595
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/Create.html
@@ -0,0 +1,87 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Create catalog") &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" enctype="multipart/form-data" id="CreateCatalog" action="Create.html">
+ <input type="hidden" name="id" value="new">
+
+ <&| /Widgets/TitleBox, title => loc("Basics"), class => "catalog-basics" &>
+ <& Elements/EditBasics, %ARGS, CatalogObj => $catalog &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Create catalog'), Name => "Update" &>
+</form>
+<%args>
+$id => ""
+</%args>
+<%init>
+my @results;
+my $catalog = RT::Catalog->new( $session{CurrentUser} );
+
+if ($id eq "new") {
+ my %create;
+ for ($catalog->WritableAttributes) {
+ $create{$_} = $ARGS{$_} if exists $ARGS{$_};
+ }
+
+ $m->callback( %ARGS, CatalogObj => $catalog, Create => \%create, CallbackName => 'MassageCreate' );
+
+ my ($ok, $msg, $nonfatal) = $catalog->Create( %create );
+ push @results, $msg, @{$nonfatal || []};
+
+ if ($ok) {
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Path => "/Admin/Assets/Catalogs/Modify.html",
+ Arguments => { id => $catalog->id },
+ );
+ }
+}
+</%init>
diff --git a/share/html/Admin/Assets/Catalogs/CustomFields.html b/share/html/Admin/Assets/Catalogs/CustomFields.html
new file mode 100644
index 0000000..a345c7d
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/CustomFields.html
@@ -0,0 +1,61 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Catalog [_1]: Edit Custom Fields", $Catalog->Name) &>
+<& /Elements/Tabs &>
+<& /Admin/Elements/EditCustomFields,
+ %ARGS,
+ Object => $Catalog,
+ ObjectType => 'RT::Catalog',
+ SubType => 'RT::Asset',
+ &>
+<%init>
+my $Catalog = LoadCatalog($id);
+</%init>
+<%args>
+$id => undef
+</%args>
diff --git a/share/html/Admin/Assets/Catalogs/Elements/EditBasics b/share/html/Admin/Assets/Catalogs/Elements/EditBasics
new file mode 100644
index 0000000..e084fb6
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/Elements/EditBasics
@@ -0,0 +1,82 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table>
+ <tr>
+ <td class="label"><label for="Name"><&|/l&>Name</&></label></td>
+ <td><input name="Name" value="<% $current{Name} %>" size="40"></td>
+ </tr>
+ <tr>
+ <td class="label"><label for="Description"><&|/l&>Description</&></label></td>
+ <td><input name="Description" value="<% $current{Description} %>" size="40"></td>
+ </tr>
+ <tr class="lifecycle">
+ <td class="label"><label for="Lifecycle"><&|/l&>Lifecycle</&></label></td>
+ <td>
+ <& /Widgets/Form/Select:InputOnly,
+ Name => 'Lifecycle',
+ Values => [ sort { loc($a) cmp loc($b) } RT::Lifecycle->List( $CatalogObj->LifecycleType ) ],
+ CurrentValue => $current{Lifecycle},
+ Default => 0,
+ &>
+ </td>
+ </tr>
+ <tr>
+ <td class="label"><label for="Disabled"><&|/l&>Disabled?</&></label></td>
+ <td>
+ <input name="Disabled" type="checkbox" value="1" <% $current{Disabled} ? "checked" : "" %>>
+ <input name="SetDisabled" type="hidden" value="1">
+ </td>
+ </tr>
+</table>
+<%args>
+$CatalogObj
+</%args>
+<%init>
+my %current = map { $_ => ($ARGS{$_} || $CatalogObj->$_ || '') }
+ $CatalogObj->WritableAttributes;
+</%init>
diff --git a/share/html/Admin/Assets/Catalogs/GroupRights.html b/share/html/Admin/Assets/Catalogs/GroupRights.html
new file mode 100644
index 0000000..333d476
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/GroupRights.html
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Catalog [_1]: Modify group rights", $Catalog->Name) &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="GroupRights.html" id="ModifyGroupRights" name="ModifyGroupRights">
+ <input type="hidden" class="hidden" name="id" value="<% $Catalog->id %>" />
+ <& /Admin/Elements/EditRights, Context => $Catalog, Principals => \@principals &>
+ <& /Elements/Submit, Label => loc('Save Changes') &>
+</form>
+<%init>
+my $Catalog = LoadCatalog($id);
+my @results = ProcessACLs(\%ARGS);
+my @principals = GetPrincipalsMap($Catalog, qw(System Roles Groups));
+</%init>
+<%args>
+$id => undef
+</%args>
diff --git a/share/html/Admin/Assets/Catalogs/Modify.html b/share/html/Admin/Assets/Catalogs/Modify.html
new file mode 100644
index 0000000..cf9a7aa
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/Modify.html
@@ -0,0 +1,88 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Catalog [_1]: Modify basics", $catalog->Name) &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" enctype="multipart/form-data" id="ModifyCatalog" action="Modify.html">
+ <input type="hidden" name="id" value="<% $catalog->id %>">
+
+ <&| /Widgets/TitleBox, title => loc("Basics"), class => "catalog-basics" &>
+ <& Elements/EditBasics, %ARGS, CatalogObj => $catalog &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save'), Name => "Update" &>
+</form>
+<%args>
+$id => undef
+$Update => 0
+</%args>
+<%init>
+my @results;
+my $catalog = LoadCatalog($id);
+
+if ($Update) {
+ my @attributes = $catalog->WritableAttributes;
+
+ # Disabled isn't submitted if unchecked, so use our hidden field to know if
+ # it was included in the form.
+ $ARGS{Disabled} = $ARGS{Disabled} ? 1 : 0
+ if $ARGS{SetDisabled};
+
+ push @results, UpdateRecordObject(
+ Object => $catalog,
+ AttributesRef => \@attributes,
+ ARGSRef => \%ARGS,
+ );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $catalog->id },
+ );
+}
+</%init>
diff --git a/share/html/Admin/Assets/Catalogs/Roles.html b/share/html/Admin/Assets/Catalogs/Roles.html
new file mode 100644
index 0000000..33bfd4f
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/Roles.html
@@ -0,0 +1,82 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Catalog [_1]: Modify roles", $catalog->Name) &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" enctype="multipart/form-data" id="ModifyCatalogRoles" action="Roles.html">
+ <input type="hidden" name="id" value="<% $catalog->id %>">
+
+ <&| /Widgets/TitleBox, title => loc("Roles"), class => "catalog-roles" &>
+ <table width="60%" class="edit">
+ <tr>
+ <td><& /Elements/Assets/EditPeople, %ARGS, Object => $catalog &></td>
+ <td><& /Elements/Assets/AddPeople, Object => $catalog &></td>
+ </tr>
+ </table>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save'), Name => "Update" &>
+</form>
+<%args>
+$id => undef
+$Update => 0
+</%args>
+<%init>
+my $catalog = LoadCatalog($id);
+my @results;
+
+if ($Update) {
+ push @results, ProcessRoleMembers( $catalog => %ARGS );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $catalog->id },
+ );
+}
+</%init>
diff --git a/share/html/Admin/Assets/Catalogs/UserRights.html b/share/html/Admin/Assets/Catalogs/UserRights.html
new file mode 100644
index 0000000..d676678
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/UserRights.html
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Catalog [_1]: Modify user rights", $Catalog->Name) &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="UserRights.html" id="ModifyUserRights" name="ModifyUserRights">
+ <input type="hidden" class="hidden" name="id" value="<% $Catalog->id %>" />
+ <& /Admin/Elements/EditRights, Context => $Catalog, Principals => \@principals &>
+ <& /Elements/Submit, Label => loc('Save Changes') &>
+</form>
+<%init>
+my $Catalog = LoadCatalog($id);
+my @results = ProcessACLs(\%ARGS);
+my @principals = GetPrincipalsMap($Catalog, qw(Users));
+</%init>
+<%args>
+$id => undef
+</%args>
diff --git a/share/html/Admin/Assets/Catalogs/index.html b/share/html/Admin/Assets/Catalogs/index.html
new file mode 100644
index 0000000..572888c
--- /dev/null
+++ b/share/html/Admin/Assets/Catalogs/index.html
@@ -0,0 +1,118 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Admin Catalogs") &>
+<& /Elements/Tabs &>
+
+<h1><% $caption %></h1>
+<p><&|/l&>Select a catalog</&>:</p>
+% unless ( $catalogs->Count ) {
+<em><&|/l&>No catalogs matching search criteria found.</&></em>
+% } else {
+<& /Elements/CollectionList,
+ OrderBy => 'Name',
+ Order => 'ASC',
+ %ARGS,
+ Format => $Format,
+ Collection => $catalogs,
+ AllowSorting => 1,
+ PassArguments => [qw(
+ Rows Page Order OrderBy FindDisabled String Op Field
+ )],
+&>
+% }
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Admin/Assets/Catalogs/index.html">
+% foreach my $field( qw(Rows Page Order OrderBy) ) {
+% next unless defined $ARGS{ $field } && length $ARGS{ $field };
+<input type="hidden" name="<% $field %>" value="<% $ARGS{ $field } %>" />
+% }
+
+<select name="Field">
+% foreach my $col (qw(id Name Description Lifecycle)) {
+<option <% $Field eq $col ? 'selected="selected"' : '' |n %> value="<% $col %>"><% loc($col) %></option>
+% }
+</select>
+<& /Elements/SelectMatch, Name => 'Op', Default => $Op &>
+<input size="20" name="String" value="<% $String %>" />
+<br />
+
+<input type="checkbox" class="checkbox" id="FindDisabled" name="FindDisabled" value="1" <% $FindDisabled ? 'checked="checked"': '' |n%> />
+<label for="FindDisabled"><&|/l&>Include disabled catalogs in listing.</&></label>
+<div align="right"><input type="submit" class="button" value="<&|/l&>Search</&>" /></div>
+</form>
+
+<%INIT>
+my $catalogs = RT::Catalogs->new($session{'CurrentUser'});
+$catalogs->FindAllRows if $FindDisabled;
+
+my ($caption);
+if ( defined $String && length $String ) {
+ $caption = $FindDisabled
+ ? loc("All catalogs matching search criteria")
+ : loc("Enabled catalogs matching search criteria");
+ $catalogs->Limit(
+ FIELD => $Field,
+ OPERATOR => $Op,
+ VALUE => $String,
+ );
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Admin/Assets/Catalogs/Modify.html?id=".$catalogs->First->id)
+ if $catalogs->Count == 1;
+} else {
+ $catalogs->UnLimit;
+ $caption = $FindDisabled
+ ? loc("All Catalogs")
+ : loc("Enabled Catalogs");
+}
+
+my $Format = RT->Config->Get('AdminSearchResultFormat')->{'Catalogs'};
+</%INIT>
+<%ARGS>
+$FindDisabled => 0
+$Field => 'Name'
+$Op => '='
+$String => ''
+</%ARGS>
diff --git a/share/html/Admin/Assets/index.html b/share/html/Admin/Assets/index.html
new file mode 100644
index 0000000..a4ac4bd
--- /dev/null
+++ b/share/html/Admin/Assets/index.html
@@ -0,0 +1,50 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('Asset Administration') &>
+<& /Elements/Tabs &>
+<& /Elements/ListMenu, menu => Menu()->child('admin')->child('assets') &>
diff --git a/share/html/Admin/Global/CustomFields/Catalog-Assets.html b/share/html/Admin/Global/CustomFields/Catalog-Assets.html
new file mode 100644
index 0000000..f85874b
--- /dev/null
+++ b/share/html/Admin/Global/CustomFields/Catalog-Assets.html
@@ -0,0 +1,54 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Admin/Elements/EditCustomFields, %ARGS, title => $title, ObjectType => "RT::Catalog", Object=> $object, SubType => "RT::Asset" &>
+<%init>
+my $title = loc('Edit Custom Fields for Assets in all Catalogs');
+my $object = RT::Catalog->new($session{'CurrentUser'});
+</%init>
diff --git a/share/html/Asset/Create.html b/share/html/Asset/Create.html
new file mode 100644
index 0000000..233c81d
--- /dev/null
+++ b/share/html/Asset/Create.html
@@ -0,0 +1,185 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Create a new asset in catalog [_1]", $catalog->Name),
+ RT->Config->Get('AssetBasicCustomFieldsOnCreate')
+ ? (
+ onload => "function () { hide('Asset-Create-details') }"
+ )
+ : (),
+ &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($catalog->Name) %>">
+<form method="post" enctype="multipart/form-data" id="CreateAsset" action="Create.html">
+ <input type="hidden" name="id" value="new">
+ <input type="hidden" name="Catalog" value="<% $catalog->id %>">
+
+<a name="basics"></a>
+<div id="Asset-Create-basics" class="asset-metadata">
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <&| /Widgets/TitleBox, title => loc("Basics"), class => "asset-basics", title_class => "inverse" &>
+ <& Elements/EditBasics, %ARGS, AssetObj => $asset, CatalogObj => $catalog &>
+ </&>
+
+ <&| /Widgets/TitleBox, title => loc("People"), class => "asset-people", title_class => "inverse" &>
+ <& Elements/EditPeople, %ARGS, AssetObj => $asset &>
+ </&>
+ </td><td>
+ <&| /Widgets/TitleBox, title => loc("Links"), class => "asset-links", title_class => "inverse" &>
+ <& /Elements/AddLinks,
+ Object => $asset,
+ CustomFields => $catalog->AssetCustomFields &>
+ </&>
+ </td>
+
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<div id="Asset-Create-details">
+<a name="details"></a>
+
+ <& /Elements/EditCustomFieldCustomGroupings,
+ Object => $asset,
+ TitleBoxARGS => { title_class => "inverse" },
+ KeepValue => 1,
+ CustomFieldGenerator => sub { $catalog->AssetCustomFields } &>
+</div>
+
+ <& /Elements/Submit, Label => loc('Create asset') &>
+</form>
+</span>
+
+<%args>
+$id => ""
+$Catalog => undef
+</%args>
+<%init>
+my $asset = RT::Asset->new( $session{CurrentUser} );
+my $catalog = RT::Catalog->new( $session{CurrentUser} );
+$catalog->Load($Catalog);
+
+Abort(loc("Unable to find catalog '[_1]'", $Catalog))
+ unless $catalog->id;
+
+Abort(loc("You don't have permission to create assets in catalog [_1].",
+ $catalog->Name || $catalog->id))
+ unless $catalog->CurrentUserHasRight("CreateAsset");
+
+# Update the current default with the latest selection
+$session{'DefaultCatalog'} = $catalog->Id;
+
+my @results;
+
+if ($id eq "new") {
+ my $skip_create = 0;
+
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $catalog->AssetCustomFields,
+ ARGSRef => \%ARGS
+ );
+
+ $m->callback(
+ CallbackName => 'BeforeCreate',
+ AssetObj => $asset,
+ CatalogObj => $catalog,
+ results => \@results,
+ cf_ok => \$cf_ok,
+ skip_create => \$skip_create,
+ ARGSRef => \%ARGS
+ );
+
+ if ($cf_ok and not $skip_create) {
+ # Handle CFs and links
+ my %create = (
+ ProcessObjectCustomFieldUpdatesForCreate(
+ ARGSRef => \%ARGS,
+ ContextObject => $catalog,
+ ),
+ ProcessLinksForCreate( ARGSRef => \%ARGS ),
+ map {
+ $_ => $ARGS{$_}
+ } $asset->Roles,
+ );
+
+ # Handle basic fields
+ for ($asset->WritableAttributes) {
+ $create{$_} = $ARGS{$_} if exists $ARGS{$_};
+ }
+
+ $m->callback( %ARGS, AssetObj => $asset, CatalogObj => $catalog, Create => \%create, CallbackName => 'MassageCreate' );
+
+ my ($ok, $msg, $nonfatal) = $asset->Create( %create );
+ push @results, $msg, @{$nonfatal || []};
+
+ if ($ok) {
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Path => "/Asset/Display.html",
+ Arguments => { id => $asset->id },
+ );
+ }
+ } else {
+ push @results, @cf_errors;
+ }
+}
+
+if ( RT->Config->Get('AssetBasicCustomFieldsOnCreate') ) {
+ PageMenu->child( basics => raw_html => q[<a href="#basics" onclick="return switchVisibility('Asset-Create-basics','Asset-Create-details');">] . loc('Basics') . q[</a>]);
+ PageMenu->child( details => raw_html => q[<a href="#details" onclick="return switchVisibility('Asset-Create-details','Asset-Create-basics');">] . loc('Details') . q[</a>]);
+}
+
+</%init>
diff --git a/share/html/Asset/CreateInCatalog.html b/share/html/Asset/CreateInCatalog.html
new file mode 100644
index 0000000..985ad37
--- /dev/null
+++ b/share/html/Asset/CreateInCatalog.html
@@ -0,0 +1,51 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header,
+ Title => loc("Create new asset") &>
+<& /Elements/Tabs &>
+<& /Asset/Elements/CreateInCatalog &>
diff --git a/share/html/Asset/CreateLinkedTicket.html b/share/html/Asset/CreateLinkedTicket.html
new file mode 100644
index 0000000..104b205
--- /dev/null
+++ b/share/html/Asset/CreateLinkedTicket.html
@@ -0,0 +1,58 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Asset => undef
+$Requestors => ''
+</%args>
+<%init>
+my $asset = LoadAsset($Asset);
+</%init>
+<& /Elements/Header,
+ Title => loc("Create linked ticket for asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+<& /Asset/Elements/CreateLinkedTicket, AssetObj => $asset, Requestors => $Requestors &>
diff --git a/share/html/Asset/Display.html b/share/html/Asset/Display.html
new file mode 100644
index 0000000..f155136
--- /dev/null
+++ b/share/html/Asset/Display.html
@@ -0,0 +1,75 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+% $m->callback(CallbackName => 'BeforeActionList', ARGSRef => \%ARGS, Asset => $asset);
+
+<& /Elements/ListActions &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<& Elements/ShowSummary, AssetObj => $asset &>
+
+% $m->callback(CallbackName => 'AfterShowSummary', ARGSRef => \%ARGS, Asset => $asset);
+
+<& /Elements/ShowHistory,
+ Object => $asset,
+ ShowDisplayModes => 0,
+ DisplayPath => 'History.html',
+ &>
+
+% $m->callback(CallbackName => 'AfterShowHistory', ARGSRef => \%ARGS, Asset => $asset);
+</span>
+
+<%args>
+$id => undef
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+$m->callback(CallbackName => 'BeforeDisplay', ARGSRef => \%ARGS, Asset => $asset);
+</%init>
diff --git a/share/html/Asset/Elements/AssetSearchBasics b/share/html/Asset/Elements/AssetSearchBasics
new file mode 100644
index 0000000..ec1742b
--- /dev/null
+++ b/share/html/Asset/Elements/AssetSearchBasics
@@ -0,0 +1,87 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&| /Widgets/TitleBox, title => loc('Basics'), class=>'asset-search-grouping asset-search-basics' &>
+<table>
+<tr class="asset-catalog"><td class="label"><label for="Catalog"><&|/l&>Catalog</&></label></td>
+ <td class="value" colspan="3">
+<& /Asset/Elements/SelectCatalog, Name => 'Catalog', CheckRight => "ShowCatalog",
+ Default => $ARGS{'Catalog'}, OnChange => "jQuery('#AssetSearch').submit()" &>
+</td></tr>
+<tr class="asset-status"><td class="label"><label for="Status"><&|/l&>Status</&></label></td>
+ <td class="value" colspan="3">
+<& /Asset/Elements/SelectStatus, Name => 'Status', CatalogObj => $CatalogObj, DefaultValue => 1,
+ Default => ($ARGS{'Status'} || '') &>
+</td></tr>
+<tr class="asset-name"><td class="label"><label for="Name"><&|/l&>Name</&></label></td>
+ <td class="value"><input type="text" id="Name" name="Name" value="<% $ARGS{Name} || ''%>" /></td>
+ <td class="label not"><label for="!Name"><&|/l&>not</&></td>
+ <td class="value"><input type="text" id="!Name" name="!Name" value="<% $ARGS{"!Name"} || ''%>" /></td></tr>
+<tr class="asset-description"><td class="label"><label for="Description"><&|/l&>Description</&></label></td>
+ <td class="value"><input type="text" id="Description" name="Description" value="<% $ARGS{Description} || ''%>" /></td>
+ <td class="label not"><label for="!Description"><&|/l&>not</&></td>
+ <td class="value"><input type="text" id="!Description" name="!Description" value="<% $ARGS{"!Description"} || ''%>" /></td></tr>
+% my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+% $CFs->LimitToCatalog( $CatalogObj->Id );
+% $CFs->LimitToObjectId(0); # LimitToGlobal but no LookupType restriction
+% $CFs->LimitToGrouping( "RT::Asset" => "Basics" );
+% while (my $cf = $CFs->Next) {
+% my $name = "CF.{" . $cf->Name . "}";
+% my $value = ref($ARGS{$name}) ? $ARGS{$name}[0] : $ARGS{$name} || '';
+% my $negval = ref($ARGS{"!$name"}) ? $ARGS{"!$name"}[0] : $ARGS{"!$name"} || '';
+<tr>
+ <td class="label"><label for="<% $name %>"><% $cf->Name %></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => $name, Default => $value &></td>
+ <td class="label not"><label for="!<% $name %>"><&|/l&>not</&></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => "!$name", Default => $negval &></td>
+</tr>
+% }
+</table>
+</&>
+<%args>
+$CatalogObj => undef
+</%args>
diff --git a/share/html/Asset/Elements/AssetSearchCFs b/share/html/Asset/Elements/AssetSearchCFs
new file mode 100644
index 0000000..fe1d17e
--- /dev/null
+++ b/share/html/Asset/Elements/AssetSearchCFs
@@ -0,0 +1,72 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&| /Widgets/TitleBox, title => $Grouping ? loc($Grouping) : loc('Custom Fields'), hide_empty => 1, class=>'asset-search-grouping asset-search-cfs' &>
+% my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+% $CFs->LimitToCatalog( $CatalogObj->Id );
+% $CFs->LimitToObjectId(0); # LimitToGlobal but no LookupType restriction
+% $CFs->LimitToGrouping( "RT::Asset" => $Grouping );
+% if ( $CFs->Count > 0 ){
+<table>
+% while (my $cf = $CFs->Next) {
+% my $name = "CF.{" . $cf->Name . "}";
+% my $value = ref($ARGS{$name}) ? $ARGS{$name}[0] : $ARGS{$name} || '';
+% my $negval = ref($ARGS{"!$name"}) ? $ARGS{"!$name"}[0] : $ARGS{"!$name"} || '';
+<tr>
+ <td class="label"><label for="<% $name %>"><% $cf->Name %></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => $name, Default => $value &></td>
+ <td class="label not"><label for="!<% $name %>"><&|/l&>not</&></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => "!$name", Default => $negval &></td>
+</tr>
+% }
+</table>
+% }
+</&>
+<%args>
+$Grouping
+$CatalogObj => undef
+</%args>
diff --git a/share/html/Asset/Elements/AssetSearchPeople b/share/html/Asset/Elements/AssetSearchPeople
new file mode 100644
index 0000000..ef4d480
--- /dev/null
+++ b/share/html/Asset/Elements/AssetSearchPeople
@@ -0,0 +1,83 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&| /Widgets/TitleBox, class => "asset-search-people", title => loc('People') &>
+<table>
+% for my $role (RT::Asset->Roles) {
+<tr class="asset-role-<% CSSClass($role) %>">
+ <td class="label"><label for="Role.<% $role %>"><% loc($role) %></td>
+ <td class="value">
+ <input type="text" id="Role.<% $role %>" name="Role.<% $role %>"
+ data-autocomplete="Users" value="<% $ARGS{"Role.$role"} || '' %>" />
+ </td>
+ <td class="label not"><label for="!Role.<% $role %>"><&|/l&>not</&></td>
+ <td class="value">
+ <input type="text" id="!Role.<% $role %>" name="!Role.<% $role %>"
+ data-autocomplete="Users" value="<% $ARGS{"!Role.$role"} || '' %>" />
+ </td>
+</tr>
+% }
+% my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+% $CFs->LimitToCatalog( $CatalogObj->Id );
+% $CFs->LimitToObjectId(0); # LimitToGlobal but no LookupType restriction
+% $CFs->LimitToGrouping( "RT::Asset" => "People" );
+% while (my $cf = $CFs->Next) {
+% my $name = "CF.{" . $cf->Name . "}";
+% my $value = ref($ARGS{$name}) ? $ARGS{$name}[0] : $ARGS{$name} || '';
+% my $negval = ref($ARGS{"!$name"}) ? $ARGS{"!$name"}[0] : $ARGS{"!$name"} || '';
+<tr>
+ <td class="label"><label for="<% $name %>"><% $cf->Name %></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => $name, Default => $value &></td>
+ <td class="label not"><label for="!<% $name %>"><&|/l&>not</&></label></td>
+ <td class="value"><& /Elements/SelectCustomFieldValue, CustomField => $cf, Name => "!$name", Default => $negval &></td>
+</tr>
+% }
+</table>
+</&>
+<%args>
+$CatalogObj => undef
+</%args>
diff --git a/share/html/Asset/Elements/CreateInCatalog b/share/html/Asset/Elements/CreateInCatalog
new file mode 100644
index 0000000..055dd9e
--- /dev/null
+++ b/share/html/Asset/Elements/CreateInCatalog
@@ -0,0 +1,53 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<form action="<% RT->Config->Get("WebPath") %>/Asset/Create.html" id="AssetCreateInCatalog">
+ <&|/l_unsafe,
+ $m->scomp("/Asset/Elements/SelectCatalog"),
+ &>Create a new asset in the catalog [_1].</&>
+ <& /Elements/Submit, Label => loc("Go"), Caption => loc("This will take you to a partially prefilled asset creation form.") &>
+</form>
diff --git a/share/html/Asset/Elements/CreateLinkedTicket b/share/html/Asset/Elements/CreateLinkedTicket
new file mode 100644
index 0000000..63a8eaa
--- /dev/null
+++ b/share/html/Asset/Elements/CreateLinkedTicket
@@ -0,0 +1,78 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$AssetObj
+$Requestors => ''
+</%args>
+<%init>
+my @description = map { $m->interp->apply_escapes($_, 'h') }
+ $AssetObj->id, $AssetObj->Name;
+</%init>
+<form action="<% RT->Config->Get("WebPath") %><% $session{CurrentUser}->Privileged ? "/Ticket" : "/SelfService" %>/Create.html" id="AssetCreateLinkedTicket">
+ <input name="new-RefersTo" value="asset:<% $AssetObj->id %>" type="hidden">
+ <input name="Subject" value="<% $AssetObj->Name %>" type="hidden">
+ <&|/l_unsafe,
+ $m->scomp("/Elements/SelectNewTicketQueue"),
+ @description &>Create a new ticket in the [_1] queue about asset #[_2]: [_3].</&>
+% if ($Requestors) {
+ <input type="hidden" name="Requestors" value="<% $Requestors%>" />
+% } else {
+% my $first = 1;
+% for my $role ($AssetObj->Roles) {
+% my $addr = $AssetObj->RoleGroup($role)->MemberEmailAddressesAsString;
+% next unless defined $addr and length $addr;
+ <br>
+ <label>
+ <input type="radio" name="Requestors" value="<% $addr %>" <% $first ? 'checked="checked"' : '' |n%>/>
+ <&|/l, loc($role), $addr &>Use asset [_1] as ticket Requestor: [_2]</&>
+ </label>
+% $first = 0;
+% }
+% }
+ <& /Elements/Submit, Label => loc("Go"), Caption => loc("This will take you to a partially prefilled ticket creation form.") &>
+</form>
diff --git a/share/html/Asset/Elements/EditBasics b/share/html/Asset/Elements/EditBasics
new file mode 100644
index 0000000..fbfde33
--- /dev/null
+++ b/share/html/Asset/Elements/EditBasics
@@ -0,0 +1,85 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table>
+% if ($AssetObj->id) {
+ <tr class="asset-catalog">
+ <td class="label"><label for="Catalog"><&|/l&>Catalog</&>:</label></td>
+ <td><& /Asset/Elements/SelectCatalog, Default => $current{Catalog} &></td>
+ </tr>
+% }
+ <tr class="asset-name">
+ <td class="label"><label for="Name"><&|/l&>Name</&>:</label></td>
+ <td><input name="Name" value="<% $current{Name} %>" size="40"></td>
+ </tr>
+ <tr class="asset-description">
+ <td class="label"><label for="Description"><&|/l&>Description</&>:</label></td>
+ <td><input name="Description" value="<% $current{Description} %>" size="40"></td>
+ </tr>
+ <tr class="asset-status">
+ <td class="label"><label for="Status"><&|/l&>Status</&>:</label></td>
+ <td><& /Asset/Elements/SelectStatus, Name => 'Status', AssetObj => $AssetObj, CatalogObj => $CatalogObj &></td>
+ </tr>
+% if ( $AssetObj->id ) {
+ <& /Elements/EditCustomFields, Object => $AssetObj, Grouping => 'Basics', InTable => 1 &>
+% } elsif ( my @cf_names = grep { defined } @{RT->Config->Get('AssetBasicCustomFieldsOnCreate') || []} ) {
+% my $cfs = $CatalogObj->AssetCustomFields;
+% for my $name ( @cf_names ) {
+% $cfs->Limit( FIELD => 'Name', VALUE => $name, CASESENSITIVE => 0 );
+% }
+ <& /Elements/EditCustomFields, Object => $AssetObj, CustomFields => $cfs, InTable => 1, KeepValue => 1 &>
+% }
+% $m->callback(%ARGS, CallbackName => "AfterFields");
+</table>
+<%args>
+$AssetObj
+$CatalogObj => undef
+</%args>
+<%init>
+my %current = map { $_ => ($ARGS{$_} || $AssetObj->$_ || '') }
+ $AssetObj->WritableAttributes;
+</%init>
diff --git a/share/html/Asset/Elements/EditDates b/share/html/Asset/Elements/EditDates
new file mode 100644
index 0000000..1e64a26
--- /dev/null
+++ b/share/html/Asset/Elements/EditDates
@@ -0,0 +1,72 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table>
+ <tr>
+ <td class="label"><&|/l&>Created</&>:</td>
+ <td>
+ <&|/l_unsafe,
+ $m->interp->apply_escapes($AssetObj->CreatedAsString, 'h'),
+ $m->scomp('/Elements/ShowUser', User => $AssetObj->CreatorObj)
+ &>[_1] by [_2]</&>
+ </td>
+ </tr>
+ <tr>
+ <td class="label"><&|/l&>Last Updated</&>:</td>
+ <td>
+ <&|/l_unsafe,
+ $m->interp->apply_escapes($AssetObj->LastUpdatedAsString, 'h'),
+ $m->scomp('/Elements/ShowUser', User => $AssetObj->LastUpdatedByObj)
+ &>[_1] by [_2]</&>
+ </td>
+ </tr>
+ <& /Elements/EditCustomFields, Object => $AssetObj, Grouping => 'Dates', InTable => 1 &>
+% $m->callback(%ARGS, CallbackName => "AfterFields");
+</table>
+<%args>
+$AssetObj
+</%args>
diff --git a/share/html/Asset/Elements/EditPeople b/share/html/Asset/Elements/EditPeople
new file mode 100644
index 0000000..6f333df
--- /dev/null
+++ b/share/html/Asset/Elements/EditPeople
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table border="0" cellpadding="0" cellspacing="0">
+% for my $role ( $AssetObj->Roles ) {
+<tr class="asset-people-<% CSSClass($role) %>">
+<td class="label">
+<% loc($role) %>:
+</td>
+<td class="value" colspan="5">
+<& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1 &>
+</td>
+</tr>
+% }
+
+</table>
+
+<%args>
+$AssetObj
+</%args>
diff --git a/share/html/Asset/Elements/SelectCatalog b/share/html/Asset/Elements/SelectCatalog
new file mode 100644
index 0000000..48e9df0
--- /dev/null
+++ b/share/html/Asset/Elements/SelectCatalog
@@ -0,0 +1,69 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/SelectObject,
+ Name => "Catalog",
+ ShowAll => $ShowAll,
+ ShowNullOption => 0,
+ CheckRight => "CreateAsset",
+ %ARGS,
+ ObjectType => "Catalog",
+ CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
+ Default => $Default,
+ &>
+<%args>
+$ShowAll => 0
+$Default => undef
+$UpdateSession => 1
+</%args>
+<%init>
+my $catalog_obj = LoadDefaultCatalog($Default || '');
+if ( $UpdateSession && $catalog_obj->Id ){
+ $session{'DefaultCatalog'} = $catalog_obj->Id;
+ $Default = $catalog_obj->Id;
+}
+</%init>
diff --git a/share/html/Asset/Elements/SelectStatus b/share/html/Asset/Elements/SelectStatus
new file mode 100644
index 0000000..f16c804
--- /dev/null
+++ b/share/html/Asset/Elements/SelectStatus
@@ -0,0 +1,66 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/SelectStatus, %ARGS &>
+<%init>
+if ($AssetObj and $AssetObj->Id) {
+ $ARGS{DefaultValue} = 0;
+ $ARGS{Default} = $DECODED_ARGS->{Status} || $ARGS{Default};
+ $ARGS{Object} = $AssetObj;
+} else {
+ my $lifecycle = ($CatalogObj || "RT::Catalog")->LifecycleObj;
+ if ( not $ARGS{DefaultValue} ){
+ $ARGS{DefaultValue} = 0;
+ $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
+ }
+ $ARGS{Statuses} = [ $AssetObj ? $lifecycle->Transitions("") : $lifecycle->Valid ];
+}
+</%init>
+<%args>
+$AssetObj => undef
+$CatalogObj => undef
+</%args>
diff --git a/share/html/Asset/Elements/ShowBasics b/share/html/Asset/Elements/ShowBasics
new file mode 100644
index 0000000..da350a1
--- /dev/null
+++ b/share/html/Asset/Elements/ShowBasics
@@ -0,0 +1,70 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table>
+ <tr class="asset-catalog">
+ <td class="label"><&|/l&>Catalog</&>:</td>
+ <td><& ShowCatalog, Asset => $AssetObj &></td>
+ </tr>
+ <tr class="asset-name">
+ <td class="label"><&|/l&>Name</&>:</td>
+ <td><% $AssetObj->Name || '' %></td>
+ </tr>
+ <tr class="asset-description">
+ <td class="label"><&|/l&>Description</&>:</td>
+ <td><% $AssetObj->Description || '' %></td>
+ </tr>
+ <tr class="asset-status">
+ <td class="label"><&|/l&>Status</&>:</td>
+ <td class="value"><% loc($AssetObj->Status) %></td>
+ </tr>
+ <& /Elements/ShowCustomFields, Object => $AssetObj, Grouping => 'Basics', Table => 0 &>
+% $m->callback(%ARGS, CallbackName => "AfterFields");
+</table>
+<%args>
+$AssetObj
+</%args>
diff --git a/share/html/Asset/Elements/ShowCatalog b/share/html/Asset/Elements/ShowCatalog
new file mode 100644
index 0000000..d7c1095
--- /dev/null
+++ b/share/html/Asset/Elements/ShowCatalog
@@ -0,0 +1,69 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<% $value %>
+<%ARGS>
+$Asset => undef
+$Catalog => undef
+</%ARGS>
+<%INIT>
+$Catalog ||= $Asset->CatalogObj if $Asset;
+return unless $Catalog;
+
+my $value = $Catalog->Name;
+
+if ( $Asset and $Asset->CurrentUserHasRight('ShowCatalog') ) {
+ # Grab the name anyway if the current user can see the catalog based on his
+ # role for this asset. Simply checking ShowCatalog on the catalog itself
+ # won't work since the role membership is on the asset. We need to
+ # implement context objects (like CFs do) for catalogs.
+ $value = $Catalog->__Value('Name');
+}
+
+$value = '#'. $Catalog->id
+ unless defined $value and length $value;
+</%INIT>
diff --git a/share/html/Asset/Elements/ShowDates b/share/html/Asset/Elements/ShowDates
new file mode 100644
index 0000000..582423e
--- /dev/null
+++ b/share/html/Asset/Elements/ShowDates
@@ -0,0 +1,72 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table>
+ <tr>
+ <td class="label"><&|/l&>Created</&>:</td>
+ <td>
+ <&|/l_unsafe,
+ $m->interp->apply_escapes($AssetObj->CreatedAsString, 'h'),
+ $m->scomp('/Elements/ShowUser', User => $AssetObj->CreatorObj)
+ &>[_1] by [_2]</&>
+ </td>
+ </tr>
+ <tr>
+ <td class="label"><&|/l&>Last Updated</&>:</td>
+ <td>
+ <&|/l_unsafe,
+ $m->interp->apply_escapes($AssetObj->LastUpdatedAsString, 'h'),
+ $m->scomp('/Elements/ShowUser', User => $AssetObj->LastUpdatedByObj)
+ &>[_1] by [_2]</&>
+ </td>
+ </tr>
+ <& /Elements/ShowCustomFields, Object => $AssetObj, Grouping => 'Dates', Table => 0 &>
+% $m->callback(%ARGS, CallbackName => "AfterFields");
+</table>
+<%args>
+$AssetObj
+</%args>
diff --git a/share/html/Asset/Elements/ShowLinks b/share/html/Asset/Elements/ShowLinks
new file mode 100644
index 0000000..3b9a6f0
--- /dev/null
+++ b/share/html/Asset/Elements/ShowLinks
@@ -0,0 +1,48 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/ShowLinks, Object => delete $ARGS{AssetObj}, %ARGS &>
diff --git a/share/html/Asset/Elements/ShowPeople b/share/html/Asset/Elements/ShowPeople
new file mode 100644
index 0000000..af4f466
--- /dev/null
+++ b/share/html/Asset/Elements/ShowPeople
@@ -0,0 +1,76 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$AssetObj
+</%args>
+<%init>
+my $CatalogObj = $AssetObj->CatalogObj;
+</%init>
+<table>
+% for my $role ($AssetObj->Roles) {
+<tr><td class="label"><% loc($role) %>:
+% if ($AssetObj->Role($role)->{Single}) {
+% my $users = $AssetObj->RoleGroup($role)->UserMembersObj(Recursively => 0);
+% $users->FindAllRows;
+% my $user = $users->Next;
+<& /Elements/ShowUser, User => $user, Link => 1 &></td></tr>
+% next if $user->id == RT->Nobody->id;
+<tr><td>
+<& ShowRoleMembers, Group => $AssetObj->RoleGroup($role), Title => 0 &>
+</div></td></tr>
+% } else {
+</td></tr><tr><td class="user-accordion">
+ <& ShowRoleMembers, Group => $AssetObj->RoleGroup($role) &>
+ <& ShowRoleMembers, Group => $CatalogObj->RoleGroup($role), Skip => $AssetObj->RoleGroup($role), Note => loc("(via this asset's catalog)") &>
+ </td></tr>
+% }
+% }
+<& /Elements/ShowCustomFields, Object => $AssetObj, Grouping => 'People', Table => 0 &>
+
+</table>
+% $m->callback( %ARGS, CallbackName => 'AfterPeople' );
diff --git a/share/html/Asset/Elements/ShowRoleMembers b/share/html/Asset/Elements/ShowRoleMembers
new file mode 100644
index 0000000..a716ca9
--- /dev/null
+++ b/share/html/Asset/Elements/ShowRoleMembers
@@ -0,0 +1,90 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+% my $users = $Group->UserMembersObj( Recursively => 0 );
+% while ( my $user = $users->Next ) {
+% next if $Skip and $Skip->HasMember( $user->PrincipalId );
+% if (not $Title) {
+% } elsif ($Single) {
+<h3><& /Elements/ShowUser, User => $user, Link => 1 &></h3>
+% next if $user->id == RT->Nobody->id;
+% } else {
+<h3><a href="#"><& /Elements/ShowUser, User => $user, Link => 0 &>
+% if ($Note) {
+ <span class="note"><% $Note %></span>
+% }
+</a>
+% if ( $session{'CurrentUser'}->Privileged ){
+<a class="user-summary" href="<%RT->Config->Get('WebPath')%>/User/Summary.html?id=<% $user->Id %>"><&|/l&>User Summary</&></a>
+% }
+</h3>
+% }
+ <div class="details">
+<& /User/Elements/UserInfo,
+ User => $user,
+ FormatConfig => 'UserAssetExtraInfo',
+ ClassPrefix => 'asset-user' &>
+% $m->callback(CallbackName => 'AfterRecord', User => $user, Group => $Group );
+ </div>
+% }
+% my $groups = $Group->GroupMembersObj( Recursively => 0 );
+% $groups->LimitToUserDefinedGroups;
+% while (my $g = $groups->Next) {
+% next if $Skip and $Skip->HasMember( $g->PrincipalId );
+<h3><a href="#"><&|/l, $g->Name &>Group: [_1]</&></a>
+% if ($Note) {
+ <span class="note"><% $Note %></span>
+% }
+</h3>
+% }
+<%ARGS>
+$Group => undef
+$Note => ''
+$Skip => undef
+$Single => 0
+$Title => 1
+</%ARGS>
diff --git a/share/html/Asset/Elements/ShowSummary b/share/html/Asset/Elements/ShowSummary
new file mode 100644
index 0000000..ebfe7f3
--- /dev/null
+++ b/share/html/Asset/Elements/ShowSummary
@@ -0,0 +1,83 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$AssetObj
+</%args>
+<%init>
+my @sections = (
+ "Basics", #loc
+ "People", #loc
+ "Dates", #loc
+ "Links", #loc
+);
+
+my $can_edit = $session{CurrentUser}->Privileged
+ && $AssetObj->CurrentUserHasRight("ModifyAsset");
+
+my %link;
+for my $section (@sections) {
+ my $page = $section eq 'Basics' ? "Modify.html" : "Modify$section.html";
+ $link{$section} =
+ RT->Config->Get("WebPath")
+ . "/Asset/$page?id="
+ . $AssetObj->id;
+}
+</%init>
+<div class="asset-metadata">
+% for my $section (@sections) {
+<&| /Widgets/TitleBox, title => loc($section), title_href => $can_edit ? $link{$section} : "", title_class => "inverse", class => "asset-\L$section" &>
+ <& "Show$section", AssetObj => $AssetObj &>
+</&>
+% }
+
+<& /Elements/ShowCustomFieldCustomGroupings,
+ Object => $AssetObj,
+ title_href => $can_edit ? RT->Config->Get("WebPath") . "/Asset/ModifyCFs.html" : "",
+ TitleBoxARGS => { title_class => "inverse" },
+ &>
+</div>
diff --git a/share/html/Asset/Elements/TSVExport b/share/html/Asset/Elements/TSVExport
new file mode 100644
index 0000000..0d34357
--- /dev/null
+++ b/share/html/Asset/Elements/TSVExport
@@ -0,0 +1,126 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%ARGS>
+$Collection
+$Format
+$PreserveNewLines => 0
+</%ARGS>
+<%ONCE>
+my $no_html = HTML::Scrubber->new( deny => '*' );
+</%ONCE>
+<%INIT>
+require HTML::Entities;
+
+$r->content_type('application/vnd.ms-excel');
+
+my $DisplayFormat = $m->comp('/Elements/ScrubHTML', Content => $Format);
+
+my @Format = $m->comp('/Elements/CollectionAsTable/ParseFormat', Format => $DisplayFormat);
+
+my @columns;
+
+my $should_loc = { map { $_ => 1 } qw(Status) };
+
+my $col_entry = sub {
+ my $col = shift;
+ # in tsv output, "#" is often a comment character but we use it for "id"
+ delete $col->{title}
+ if $col->{title} and $col->{title} =~ /^\s*#\s*$/;
+ return {
+ header => Encode::encode_utf8(loc($col->{title} || $col->{attribute})),
+ map => $m->comp(
+ "/Elements/ColumnMap",
+ Class => "RT__Asset",
+ Name => $col->{attribute},
+ Attr => 'value'
+ ),
+ should_loc => $should_loc->{$col->{attribute}},
+ }
+};
+
+if ($PreserveNewLines) {
+ my $col = [];
+ push @columns, $col;
+ for (@Format) {
+ if ($_->{title} eq 'NEWLINE') {
+ $col = [];
+ push @columns, $col;
+ }
+ else {
+ push @$col, $col_entry->($_);
+ }
+ }
+}
+else {
+ push @columns, [map { $_->{attribute}
+ ? $col_entry->($_)
+ : () } @Format];
+}
+
+for (@columns) {
+ $m->out(join("\t", map { $_->{header} } @$_)."\n");
+}
+
+my $ii = 0;
+while (my $row = $Collection->Next) {
+ for my $col (@columns) {
+ $m->out(join("\t", map {
+ my $val = ProcessColumnMapValue($_->{map}, Arguments => [$row, $ii++], Escape => 0);
+ $val = loc($val) if $_->{should_loc};
+ # remove tabs from all field values, they screw up the tsv
+ $val = '' unless defined $val;
+ $val =~ s/(?:\n|\r)//g; $val =~ s{\t}{ }g;
+ $val = $no_html->scrub($val);
+ $val = HTML::Entities::decode_entities($val);
+ Encode::encode_utf8($val);
+ } @$col)."\n");
+ }
+}
+$m->abort();
+
+</%INIT>
diff --git a/share/html/Asset/Helpers/CreateInCatalog b/share/html/Asset/Helpers/CreateInCatalog
new file mode 100644
index 0000000..034ed91
--- /dev/null
+++ b/share/html/Asset/Helpers/CreateInCatalog
@@ -0,0 +1,49 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Asset/Elements/CreateInCatalog &>
+% $m->abort;
diff --git a/share/html/Asset/Helpers/CreateLinkedTicket b/share/html/Asset/Helpers/CreateLinkedTicket
new file mode 100644
index 0000000..95228f5
--- /dev/null
+++ b/share/html/Asset/Helpers/CreateLinkedTicket
@@ -0,0 +1,56 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Asset
+$Requestors => ''
+</%args>
+<%init>
+my $asset = LoadAsset($Asset);
+</%init>
+<& /Asset/Elements/CreateLinkedTicket, AssetObj => $asset, Requestors => $Requestors &>
+% $m->abort;
diff --git a/share/html/Asset/History.html b/share/html/Asset/History.html
new file mode 100644
index 0000000..925eeb4
--- /dev/null
+++ b/share/html/Asset/History.html
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("History of Asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<& /Elements/ShowHistory,
+ Object => $asset,
+ ShowDisplayModes => 0,
+ DisplayPath => 'History.html',
+ &>
+</span>
+
+<%args>
+$id => undef
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+</%init>
diff --git a/share/html/Asset/Modify.html b/share/html/Asset/Modify.html
new file mode 100644
index 0000000..f760268
--- /dev/null
+++ b/share/html/Asset/Modify.html
@@ -0,0 +1,133 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Modify asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<form method="post" enctype="multipart/form-data" id="ModifyAsset" action="Modify.html">
+ <input type="hidden" name="id" value="<% $asset->id %>">
+ <input type="hidden" name="DisplayAfter" value="<% $DisplayAfter ? 1 : 0 %>">
+
+ <&| /Widgets/TitleBox, title => loc("Basics"), class => "asset-basics", title_class => "inverse" &>
+ <& Elements/EditBasics, %ARGS, AssetObj => $asset &>
+ </&>
+
+ <&| /Widgets/TitleBox, title => loc("Custom Fields"), class => "asset-info-cfs", title_class => "inverse" &>
+ <& /Elements/EditCustomFields, Object => $asset, Grouping => '' &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save asset'), Name => "Update" &>
+</form>
+</span>
+
+<%args>
+$id => undef
+$Update => 0
+$DisplayAfter => 0
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+my @results;
+
+$m->callback( Asset => $asset, ARGSRef => \%ARGS, Update => \$Update, results => \@results );
+
+if ($Update) {
+ my $skip_update = 0;
+ my @attributes = $asset->WritableAttributes;
+
+ # Don't update status unless we have a value; otherwise RT complains
+ @attributes = grep { $_ ne "Status" } @attributes
+ unless $ARGS{Status};
+
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $asset->CustomFields,
+ ARGSRef => \%ARGS
+ );
+
+ $m->callback(
+ CallbackName => 'BeforeUpdate',
+ AssetObj => $asset,
+ UpdatableAttributes => \@attributes,
+ results => \@results,
+ cf_ok => \$cf_ok,
+ skip_update => \$skip_update,
+ ARGSRef => \%ARGS,
+ );
+
+ if ($cf_ok and not $skip_update) {
+ push @results, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+ push @results, UpdateRecordObject(
+ Object => $asset,
+ AttributesRef => \@attributes,
+ ARGSRef => \%ARGS
+ );
+
+ $m->callback(
+ CallbackName => 'AfterUpdate',
+ AssetObj => $asset,
+ UpdatableAttributes => \@attributes,
+ results => \@results,
+ ARGSRef => \%ARGS,
+ );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $asset->id },
+ $DisplayAfter
+ ? (Path => "/Asset/Display.html")
+ : (),
+ );
+ } else {
+ push @results, @cf_errors;
+ }
+}
+</%init>
diff --git a/share/html/Asset/ModifyCFs.html b/share/html/Asset/ModifyCFs.html
new file mode 100755
index 0000000..41728ca
--- /dev/null
+++ b/share/html/Asset/ModifyCFs.html
@@ -0,0 +1,105 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Modify [_3] for asset #[_1]: [_2]", $asset->id, $asset->Name, $Grouping || loc("Custom Fields") ) &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<form method="post" enctype="multipart/form-data" id="ModifyAssetCFs" action="ModifyCFs.html">
+ <input type="hidden" name="id" value="<% $asset->id %>">
+ <input type="hidden" name="Grouping" value="<% $Grouping %>">
+
+ <&| /Widgets/TitleBox, title => $Grouping ? loc($Grouping) : loc("Custom Fields"), class => "asset-info-cfs ".($Grouping ? CSSClass("asset-info-cfs-\L$Grouping") : ""), title_class => "inverse" &>
+ <& /Elements/EditCustomFields, Object => $asset, Grouping => $Grouping &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save asset'), Name => "Update" &>
+</form>
+</span>
+
+<%args>
+$id => undef
+$Update => 0
+$Grouping => ''
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+my @results;
+
+my $CFs = $asset->CustomFields;
+$CFs->LimitToGrouping($asset => $Grouping);
+
+Abort(loc("No custom fields found for grouping '[_1]'", $Grouping))
+ if $Grouping and not $CFs->Count;
+
+$m->callback( Asset => $asset, ARGSRef => \%ARGS, Update => \$Update, results => \@results );
+
+if ($Update) {
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $CFs,
+ ARGSRef => \%ARGS
+ );
+
+ if ($cf_ok) {
+ push @results, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => {
+ id => $asset->id,
+ Grouping => $Grouping,
+ },
+ );
+ } else {
+ push @results, @cf_errors;
+ }
+}
+</%init>
diff --git a/share/html/Asset/ModifyDates.html b/share/html/Asset/ModifyDates.html
new file mode 100755
index 0000000..8ca0f3e
--- /dev/null
+++ b/share/html/Asset/ModifyDates.html
@@ -0,0 +1,97 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Modify dates for asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<form method="post" enctype="multipart/form-data" id="ModifyAssetDates" action="ModifyDates.html">
+ <input type="hidden" name="id" value="<% $asset->id %>">
+
+ <&| /Widgets/TitleBox, title => loc("Dates"), class => "asset-dates", title_class => "inverse" &>
+ <& Elements/EditDates, %ARGS, AssetObj => $asset &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save asset'), Name => "Update" &>
+</form>
+</span>
+
+<%init>
+my $asset = LoadAsset($id);
+my @results;
+
+$m->callback( Asset => $asset, ARGSRef => \%ARGS, Update => \$Update, results => \@results );
+
+if ($Update) {
+ my $CFs = $asset->CustomFields;
+ $CFs->LimitToGrouping($asset => "Dates");
+
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $CFs,
+ ARGSRef => \%ARGS
+ );
+
+ if ($cf_ok) {
+ push @results, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $asset->id },
+ );
+ } else {
+ push @results, @cf_errors;
+ }
+}
+</%init>
+<%args>
+$id => undef
+$Update => 0
+</%args>
diff --git a/share/html/Asset/ModifyLinks.html b/share/html/Asset/ModifyLinks.html
new file mode 100755
index 0000000..ad95b8f
--- /dev/null
+++ b/share/html/Asset/ModifyLinks.html
@@ -0,0 +1,98 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Modify links for asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<form method="post" enctype="multipart/form-data" id="ModifyAssetLinks" action="ModifyLinks.html">
+ <input type="hidden" name="id" value="<% $asset->id %>">
+
+ <&| /Widgets/TitleBox, title => loc("Links"), class => "asset-links", title_class => "inverse" &>
+ <& /Elements/EditLinks, %ARGS, Object => $asset, Merge => 0 &>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save asset'), Name => "Update" &>
+</form>
+</span>
+
+<%init>
+my $asset = LoadAsset($id);
+my @results;
+
+$m->callback( Asset => $asset, ARGSRef => \%ARGS, Update => \$Update, results => \@results );
+
+if ($Update) {
+ my $CFs = $asset->CustomFields;
+ $CFs->LimitToGrouping($asset => "Links");
+
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $CFs,
+ ARGSRef => \%ARGS
+ );
+
+ if ($cf_ok) {
+ push @results, ProcessRecordLinks( RecordObj => $asset, ARGSRef => \%ARGS );
+ push @results, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $asset->id },
+ );
+ } else {
+ push @results, @cf_errors;
+ }
+}
+</%init>
+<%args>
+$id => undef
+$Update => 0
+</%args>
diff --git a/share/html/Asset/ModifyPeople.html b/share/html/Asset/ModifyPeople.html
new file mode 100755
index 0000000..330f530
--- /dev/null
+++ b/share/html/Asset/ModifyPeople.html
@@ -0,0 +1,111 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Modify people related to asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+
+% $m->callback(CallbackName => 'BeforeActionList', ARGSRef => \%ARGS, Asset => $asset, Actions => \@results);
+
+<& /Elements/ListActions, actions => \@results &>
+
+<span class="catalog <% CSSClass($asset->CatalogObj->Name) %>">
+<form method="post" enctype="multipart/form-data" id="ModifyAssetPeople" action="ModifyPeople.html">
+ <input type="hidden" name="id" value="<% $asset->id %>">
+
+ <&| /Widgets/TitleBox, title => loc("People"), class => "asset-people", title_class => "inverse" &>
+ <table width="100%" class="edit">
+ <tr>
+ <td width="30%"><& /Elements/Assets/EditPeople, %ARGS, Object => $asset &></td>
+ <td width="30%"><& /Elements/Assets/AddPeople, Object => $asset &></td>
+ <td>
+ <& /Elements/EditCustomFields, Object => $asset, Grouping => 'People', AsTable => 1 &>
+ </td>
+ </tr>
+ </table>
+ </&>
+
+ <& /Elements/Submit, Label => loc('Save'), Name => "Update" &>
+</form>
+</span>
+
+<%init>
+my $asset = LoadAsset($id);
+my @results;
+
+$m->callback( Asset => $asset, ARGSRef => \%ARGS, Update => \$Update, results => \@results );
+
+if ($Update) {
+ my $CFs = $asset->CustomFields;
+ $CFs->LimitToGrouping($asset => "People");
+
+ my ($cf_ok, @cf_errors) = $m->comp(
+ '/Elements/ValidateCustomFields',
+ Object => $asset,
+ CustomFields => $CFs,
+ ARGSRef => \%ARGS
+ );
+
+ if ($cf_ok) {
+ push @results, ProcessRoleMembers( $asset => %ARGS );
+ push @results, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $asset->id },
+ );
+ } else {
+ push @results, @cf_errors;
+ }
+}
+
+$m->callback(CallbackName => 'BeforeDisplay', ARGSRef => \%ARGS, Asset => $asset);
+</%init>
+<%args>
+$id => undef
+$Update => 0
+</%args>
+
diff --git a/share/html/Asset/Search/Bulk.html b/share/html/Asset/Search/Bulk.html
new file mode 100644
index 0000000..74fe7db
--- /dev/null
+++ b/share/html/Asset/Search/Bulk.html
@@ -0,0 +1,197 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc("Assets") &>
+<& /Elements/Tabs &>
+
+% $m->callback(CallbackName => 'BeforeActionList', ARGSRef => \%ARGS, Assets => $assets, Actions => \@results);
+
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Asset/Search/Bulk.html" enctype="multipart/form-data" name="BulkUpdate" id="BulkUpdate">
+% foreach my $var ( @{$search{'PassArguments'}} ) {
+<input type="hidden" class="hidden" name="<% $var %>" value="<% $ARGS{$var} || '' %>" />
+% }
+<& /Elements/CollectionList,
+ %search,
+ Collection => $assets,
+ AllowSorting => 1,
+ DisplayFormat => $DisplayFormat,
+ &>
+% if (not $assets->Count) {
+<em><&|/l&>No assets matching search criteria found.</&></em>
+% }
+
+<& /Elements/Submit,
+ Name => 'Update',
+ Label => loc('Update'),
+ CheckboxNameRegex => '/^UpdateAsset(All)?$/',
+ CheckAll => 1, ClearAll => 1,
+&>
+
+<&| /Widgets/TitleBox, title => loc("Basics"), class => "asset-basics asset-bulk-basics", title_class => "inverse" &>
+<table>
+ <tr class="asset-catalog">
+ <td class="label"><label for="UpdateCatalog"><&|/l&>Catalog</&></label></td>
+ <td><& /Asset/Elements/SelectCatalog, Name => 'UpdateCatalog', Default => $catalog_obj->id, UpdateSession => 0, &></td>
+ </tr>
+ <tr class="asset-name">
+ <td class="label"><label for="UpdateName"><&|/l&>Name</&></label></td>
+ <td><input name="UpdateName" value="<% $ARGS{'Name'}||'' %>" size="40"></td>
+ </tr>
+ <tr class="asset-description">
+ <td class="label"><label for="UpdateDescription"><&|/l&>Description</&></label></td>
+ <td><input name="UpdateDescription" value="<% $ARGS{'Description'}||'' %>" size="40"></td>
+ </tr>
+ <tr class="asset-status">
+ <td class="label"><label for="UpdateStatus"><&|/l&>Status</&></label></td>
+ <td><& /Asset/Elements/SelectStatus, Name => 'UpdateStatus', DefaultValue => 1, CatalogObj => $catalog_obj &></td>
+ </tr>
+</table>
+</&>
+
+<&| /Widgets/TitleBox, title => loc("People"), class => "asset-people asset-bulk-people", title_class => "inverse" &>
+<table>
+% for my $rname ( $asset->Roles( ACLOnly => 0 ) ) {
+% my $role = $asset->Role( $rname );
+% if ( $role->{'Single'} ) {
+% my $input = "SetRoleMember-$rname";
+<tr class="full-width">
+<td class="label"><label for="<% $input %>"><% loc($rname) %></label></td>
+<td><input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" /></td>
+</tr>
+% } else {
+<tr>
+% my $input = "AddRoleMember-$rname";
+<td class="label"><label for="<% $input %>"><% loc("Add [_1]", loc($rname)) %></label></td>
+<td><input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" /></td>
+</tr>
+
+<tr>
+% $input = "RemoveRoleMember-$rname";
+<td class="label"><label for="<% $input %>"><% loc("Remove [_1]", loc($rname)) %></label></td>
+<td>
+ <input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" />
+ <label>
+ <input type="checkbox" name="RemoveAllRoleMembers-<% $rname %>" value="1"/>
+ <em><&|/l&>(Check to delete all values)</&></em>
+ </label>
+</td>
+</tr>
+% }
+% }
+</table>
+</&>
+
+% for my $group ( RT::CustomField->CustomGroupings( 'RT::Asset' ), '' ) {
+% my $cfs = $catalog_obj->AssetCustomFields;
+% $cfs->LimitToGrouping( 'RT::Asset' => $group);
+% if ( $cfs->Count ) {
+<&| /Widgets/TitleBox, class=>'asset-bulk-grouping asset-bulk-cfs', title => loc('Edit [_1]', ($group? loc($group) : loc('Custom Fields')) ) &>
+<& /Elements/BulkCustomFields, CustomFields => $cfs, &>
+</&>
+% }
+% }
+
+<&|/Widgets/TitleBox, title => loc('Edit Links'), color => "#336633"&>
+<& /Elements/BulkLinks, Collection => $assets, %ARGS &>
+</&>
+
+<& /Elements/Submit, Label => loc('Update'), Name => 'Update' &>
+</form>
+
+<%INIT>
+my @results;
+$m->callback(ARGSRef => \%ARGS, Results => \@results, CallbackName => 'Initial');
+
+my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+$ARGS{'Catalog'} = $catalog_obj->Id;
+
+my $assets = RT::Assets->new($session{CurrentUser});
+my %search = ProcessAssetsSearchArguments(
+ Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+);
+
+my $DisplayFormat = "'__CheckBox.{UpdateAsset}__',". $search{'Format'};
+$DisplayFormat =~ s/\s*,\s*('?__NEWLINE__'?)/,$1,''/gi;
+
+my $asset = RT::Asset->new( $session{'CurrentUser'} );
+
+delete $ARGS{$_} foreach grep { $ARGS{$_} =~ /^$/ } keys %ARGS;
+
+$DECODED_ARGS->{'UpdateAssetAll'} = 1 unless @UpdateAsset;
+
+if ( $ARGS{Update} ) {
+ my @attributes = $asset->WritableAttributes;
+ @attributes = grep exists $ARGS{ 'Update'. $_ }, @attributes;
+ my %basics = map { $_ => $ARGS{ 'Update'. $_ } } @attributes;
+
+ foreach my $aid ( @UpdateAsset ) {
+ my $asset = LoadAsset($aid);
+
+ my @tmp_res;
+ push @tmp_res, UpdateRecordObject(
+ Object => $asset,
+ AttributesRef => \@attributes,
+ ARGSRef => \%basics,
+ );
+ push @tmp_res, ProcessRoleMembers( $asset => %ARGS );
+ push @tmp_res, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+ push @tmp_res, ProcessRecordLinks( RecordObj => $asset, RecordId => 'Asset', ARGSRef => \%ARGS );
+ push @tmp_res, ProcessRecordBulkCustomFields( RecordObj => $asset, ARGSRef => \%ARGS );
+ push @results, map { loc( "Asset #[_1]: [_2]", $asset->id, $_ ) } @tmp_res;
+ }
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } @{$search{'PassArguments'}} },
+ );
+}
+</%INIT>
+<%ARGS>
+ at UpdateAsset => ()
+</%ARGS>
diff --git a/share/html/Asset/Search/Results.tsv b/share/html/Asset/Search/Results.tsv
new file mode 100644
index 0000000..41248b6
--- /dev/null
+++ b/share/html/Asset/Search/Results.tsv
@@ -0,0 +1,73 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%init>
+my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+$ARGS{'Catalog'} = $catalog_obj->Id;
+
+my $assets = RT::Assets->new($session{CurrentUser});
+ProcessAssetsSearchArguments(
+ Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+);
+
+my $Format = q|id, Name, Description, Status, Catalog, |;
+
+$Format .= "$_, " for RT::Asset->Roles;
+
+my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+$CFs->LimitToCatalog( $catalog_obj->Id );
+$CFs->LimitToObjectId( 0 ); # LimitToGlobal but no LookupType restriction
+$Format .= "'__CF.{$_}__/TITLE:$_', " for map {$_ = $_->Name; s/['\\]/\\$1/g; $_} @{$CFs->ItemsArrayRef};
+
+$m->callback(CallbackName => "ModifyFormat", Format => \$Format );
+
+my $comp = "/Asset/Elements/TSVExport";
+$comp = "/Elements/TSVExport" if $m->comp_exists("/Elements/TSVExport");
+
+$m->comp($comp, Collection => $assets, Format => $Format );
+
+</%init>
diff --git a/share/html/Asset/Search/index.html b/share/html/Asset/Search/index.html
new file mode 100644
index 0000000..a0eb3fd
--- /dev/null
+++ b/share/html/Asset/Search/index.html
@@ -0,0 +1,108 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%init>
+my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+$ARGS{'Catalog'} = $catalog_obj->Id;
+
+my $assets = RT::Assets->new($session{CurrentUser});
+my %search = ProcessAssetsSearchArguments(
+ Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+);
+
+my $title = ( $ARGS{'SearchAssets'} or $ARGS{q} ) ?
+ loc("Found [quant,_1,asset,assets]",$assets->Count)
+ : loc("Assets");
+
+</%init>
+<& /Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+
+% if ( $ARGS{'SearchAssets'} or $ARGS{q} ){
+<& /Elements/CollectionList,
+ %search,
+ Collection => $assets,
+ AllowSorting => 1,
+ &>
+% if (not $assets->Count) {
+<em><&|/l&>No assets matching search criteria found.</&></em>
+% }
+% }
+<span class="catalog <% CSSClass( $catalog_obj->Name ) %>">
+<form action="<% RT->Config->Get('WebPath') %>/Asset/Search/index.html" id="AssetSearch">
+<&| /Widgets/TitleBox, title => loc("Search Assets") &>
+<& /Asset/Elements/AssetSearchBasics, %ARGS, CatalogObj => $catalog_obj &>
+<& /Asset/Elements/AssetSearchPeople, %ARGS, CatalogObj => $catalog_obj &>
+<& /Elements/Submit, Label => loc('Search'), Name => 'SearchAssets' &>
+
+% foreach my $group ( 'Dates', 'Links', RT::CustomField->CustomGroupings( "RT::Asset" ), '' ) {
+ <& /Asset/Elements/AssetSearchCFs, %ARGS, Grouping => $group,
+ CatalogObj => $catalog_obj &>
+% }
+<& /Elements/Submit, Label => loc('Search'), Name => 'SearchAssets' &>
+</&>
+
+<script>
+jQuery(function() {
+ var all_inputs = jQuery("#AssetSearch input, #AssetSearch select");
+ all_inputs.each(function() {
+ var elem = jQuery(this);
+ var update_elems = all_inputs.filter(function () {
+ return jQuery(this).attr("name") == elem.attr("name");
+ }).not(elem);
+ if (update_elems.length == 0)
+ return;
+ var trigger_func = function() { update_elems.val(elem.val()) };
+ if (elem.attr("type") == "text")
+ elem.keyup( trigger_func );
+ else
+ elem.change( trigger_func );
+ });
+});
+</script>
+</form>
+</span>
diff --git a/share/html/Asset/index.html b/share/html/Asset/index.html
new file mode 100644
index 0000000..ebb2299
--- /dev/null
+++ b/share/html/Asset/index.html
@@ -0,0 +1,66 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%init>
+my $assets = RT::Assets->new($session{CurrentUser});
+$assets->UnLimit;
+</%init>
+<& /Elements/Header, Title => loc("Assets") &>
+<& /Elements/Tabs &>
+
+<& /Elements/CollectionList,
+ Collection => $assets,
+ OrderBy => 'Name',
+ Order => 'ASC',
+ Format => q[
+ '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
+ '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
+ Description,
+ Status,
+ ],
+ AllowSorting => 0,
+ &>
diff --git a/share/html/Elements/AddLinks b/share/html/Elements/AddLinks
index 3e34237..225fd4a 100644
--- a/share/html/Elements/AddLinks
+++ b/share/html/Elements/AddLinks
@@ -61,6 +61,7 @@ $exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
% if (ref($Object) eq 'RT::Ticket') {
<i><&|/l&>Enter tickets or URIs to link tickets to. Separate multiple entries with spaces.</&>
<br /><&|/l&>You may enter links to Articles as "a:###", where ### represents the number of the Article.</&>
+<br /><&|/l&>Enter links to assets as "asset:###", where ### represents the asset ID.</&>
% $m->callback( CallbackName => 'ExtraLinkInstructions' );
</i><br />
% } elsif (ref($Object) eq 'RT::Queue') {
diff --git a/share/html/Elements/Assets/AddPeople b/share/html/Elements/Assets/AddPeople
new file mode 100644
index 0000000..e73ec5e
--- /dev/null
+++ b/share/html/Elements/Assets/AddPeople
@@ -0,0 +1,67 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Object
+</%args>
+<div class="add-user">
+<h3><&|/l&>Add a person</&></h3>
+<& SelectRoleType, Object => $Object, Name => "AddUserRoleMember-Role" &>
+<input type="text" name="AddUserRoleMember"
+ data-autocomplete="Users"
+ data-autocomplete-return="Name"
+ placeholder="<% loc("Find a user...") %>">
+</div>
+
+<div class="add-group">
+<h3><&|/l&>Add a group</&></h3>
+<& SelectRoleType, Object => $Object, Name => "AddGroupRoleMember-Role" &>
+<input type="text" name="AddGroupRoleMember"
+ data-autocomplete="Groups"
+ data-autocomplete-return="Name"
+ placeholder="<% loc("Find a group...") %>">
+</div>
diff --git a/share/html/Elements/Assets/EditPeople b/share/html/Elements/Assets/EditPeople
new file mode 100644
index 0000000..6f9ccdd
--- /dev/null
+++ b/share/html/Elements/Assets/EditPeople
@@ -0,0 +1,59 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Object
+</%args>
+<%init>
+</%init>
+% for my $role ($Object->Roles( ACLOnly => 0 )) {
+<div class="role-<% CSSClass($role) %> role">
+ <h3><% loc($role) %></h3>
+ <& EditRoleMembers, Group => $Object->RoleGroup($role) &>
+</div>
+% }
+<em><&|/l&>(Check box to delete)</&></em>
diff --git a/share/html/Elements/Assets/EditRoleMembers b/share/html/Elements/Assets/EditRoleMembers
new file mode 100644
index 0000000..30b6962
--- /dev/null
+++ b/share/html/Elements/Assets/EditRoleMembers
@@ -0,0 +1,79 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Group => undef
+$Recursively => 0
+</%args>
+<%init>
+my $field_name = "RemoveRoleMember-" . $Group->Name;
+</%init>
+<ul class="role-members">
+% my $Users = $Group->UserMembersObj( Recursively => $Recursively );
+% if ($Group->SingleMemberRoleGroup) {
+<input type="text" value="<% $Users->First->Name %>" name="SetRoleMember-<% $Group->Name %>" id="SetRoleMember-<% $Group->Name %>" data-autocomplete="Users" data-autocomplete-return="Name" /><br />
+% } else {
+% while ( my $user = $Users->Next ) {
+<li>
+ <label>
+ <input type="checkbox" name="<% $field_name %>" value="<% $user->PrincipalId %>">
+ <& /Elements/ShowUser, User => $user &>
+ </label>
+</li>
+% }
+% my $Groups = $Group->GroupMembersObj( Recursively => $Recursively );
+% $Groups->LimitToUserDefinedGroups;
+% while (my $group = $Groups->Next) {
+<li>
+ <label>
+ <input type="checkbox" name="<% $field_name %>" value="<% $group->PrincipalId %>">
+ <&|/l&>Group</&>: <% $group->Name %>
+ </label>
+</li>
+% }
+% }
+</ul>
diff --git a/share/html/Elements/Assets/Search b/share/html/Elements/Assets/Search
new file mode 100644
index 0000000..9750ef766
--- /dev/null
+++ b/share/html/Elements/Assets/Search
@@ -0,0 +1,53 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<form ACTION="<% RT->Config->Get('WebPath') %>/Asset/Search/">
+ <input size="12" name="q" accesskey="0" class="field" value="<% $value %>" placeholder="<&|/l&>Search Assets</&>..." />
+</form>
+<%init>
+my $value = defined $DECODED_ARGS->{q} ? $DECODED_ARGS->{q} : '';
+</%init>
diff --git a/share/html/Elements/Assets/SelectRoleType b/share/html/Elements/Assets/SelectRoleType
new file mode 100644
index 0000000..a38e305
--- /dev/null
+++ b/share/html/Elements/Assets/SelectRoleType
@@ -0,0 +1,60 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Object
+$Name
+$AllowNull => 0
+</%args>
+<select name="<% $Name %>">
+% if ($AllowNull) {
+ <option value=""></option>
+% }
+% for my $role ($Object->Roles( ACLOnly => 0, Single => 0 )) {
+ <option value="<% $role %>"><% loc($role) %></option>
+% }
+</select>
diff --git a/share/html/Elements/FindAsset b/share/html/Elements/FindAsset
new file mode 100644
index 0000000..d7e8b1f
--- /dev/null
+++ b/share/html/Elements/FindAsset
@@ -0,0 +1,53 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&|/Widgets/TitleBox, title => loc('Find an asset') &>
+<form action="<% RT->Config->Get('WebPath') %>/Asset/Search/">
+ <input type="text" name="q" />
+ <input type="submit" value="<&|/l&>Search</&>" class="button" />
+</form>
+</&>
diff --git a/share/html/Elements/MyAssets b/share/html/Elements/MyAssets
new file mode 100644
index 0000000..615ffcf
--- /dev/null
+++ b/share/html/Elements/MyAssets
@@ -0,0 +1,48 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /User/Elements/AssetList, User => $session{'CurrentUser'}->UserObj, Roles => [qw(HeldBy)], Title => loc('My Assets') &>
diff --git a/share/html/Elements/RT__Asset/ColumnMap b/share/html/Elements/RT__Asset/ColumnMap
new file mode 100644
index 0000000..ffc8b0e
--- /dev/null
+++ b/share/html/Elements/RT__Asset/ColumnMap
@@ -0,0 +1,121 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%ARGS>
+$Name => undef
+$Attr => undef
+</%ARGS>
+<%ONCE>
+my $linkUsers;
+$linkUsers = sub {
+ my ($what, $more) = @_;
+ if ($what->isa("RT::Group")) {
+ # Link the users (non-recursively)
+ my @ret = map {$linkUsers->($_->[1], $more), ", "}
+ sort {$a->[0] cmp $b->[0]}
+ map {+[($_->EmailAddress||''), $_]}
+ @{ $what->UserMembersObj( Recursively => 0 )->ItemsArrayRef };
+
+ # But don't link the groups
+ push @ret, map {+("Group: $_", ",")}
+ sort map {$_->Name} @{ $what->GroupMembersObj( Recursively => 0)->ItemsArrayRef };
+
+ pop @ret; # Remove ending ", "
+ return @ret;
+ } else {
+ my @ret = \($m->scomp("/Elements/ShowUser", User => $what));
+ push @ret, $more->($what) if $more;
+ return @ret;
+ }
+};
+my $COLUMN_MAP = {
+ Name => {
+ attribute => 'Name',
+ title => 'Name',
+ value => sub { $_[0]->Name },
+ },
+ Description => {
+ attribute => 'Description',
+ title => 'Description',
+ value => sub { $_[0]->Description },
+ },
+ Catalog => {
+ attribute => 'Catalog',
+ title => 'Catalog', # loc
+ value => sub { $_[0]->CatalogObj->Name },
+ },
+ Status => {
+ title => 'Status',
+ attribute => 'Status',
+ value => sub { loc($_[0]->Status) }
+ },
+ ActiveTickets => {
+ title => 'Active tickets', # loc
+ value => sub {
+ my $Asset = shift;
+ my $Query = "RefersTo = 'asset:" . $Asset->id . "'";
+ $Query .= " AND (" . join(" OR ", map { "Status = '$_'" } RT::Queue->ActiveStatusArray) . ")";
+ my $SearchURL = RT->Config->Get('WebPath') . '/Search/Results.html?' . $m->comp('/Elements/QueryString', Query => $Query);
+ return \'[ <a href="',$SearchURL,\'">Active</a> ]';
+ }
+ },
+ InactiveTickets => {
+ title => 'Inactive tickets', # loc
+ value => sub {
+ my $Asset = shift;
+ my $Query = "RefersTo = 'asset:" . $Asset->id . "'";
+ $Query .= " AND (" . join(" OR ", map { "Status = '$_'" } RT::Queue->InactiveStatusArray) . ")";
+ my $SearchURL = RT->Config->Get('WebPath') . '/Search/Results.html?' . $m->comp('/Elements/QueryString', Query => $Query);
+ return \'[ <a href="',$SearchURL,\'">Inactive</a> ]';
+ }
+ },
+};
+</%ONCE>
+<%init>
+$m->callback( COLUMN_MAP => $COLUMN_MAP, CallbackName => 'Once', CallbackOnce => 1 );
+return GetColumnMapEntry( Map => $COLUMN_MAP, Name => $Name, Attribute => $Attr );
+</%init>
diff --git a/share/html/Elements/RT__Catalog/ColumnMap b/share/html/Elements/RT__Catalog/ColumnMap
new file mode 100644
index 0000000..8aa1bd6
--- /dev/null
+++ b/share/html/Elements/RT__Catalog/ColumnMap
@@ -0,0 +1,79 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%ARGS>
+$Name => undef
+$Attr => undef
+</%ARGS>
+<%ONCE>
+my $COLUMN_MAP = {
+ Name => {
+ attribute => 'Name',
+ title => 'Name',
+ value => sub { $_[0]->Name },
+ },
+ Description => {
+ attribute => 'Description',
+ title => 'Description',
+ value => sub { $_[0]->Description },
+ },
+ Disabled => {
+ title => \' ',
+ attribute => 'Disabled',
+ value => sub { return $_[0]->Disabled? $_[0]->loc('Disabled'): $_[0]->loc('Enabled') },
+ },
+ Lifecycle => {
+ title => 'Lifecycle',
+ attribute => 'Lifecycle',
+ value => sub { return $_[0]->Lifecycle },
+ },
+};
+</%ONCE>
+<%init>
+$m->callback( COLUMN_MAP => $COLUMN_MAP, CallbackName => 'Once', CallbackOnce => 1 );
+return GetColumnMapEntry( Map => $COLUMN_MAP, Name => $Name, Attribute => $Attr );
+</%init>
diff --git a/share/html/Elements/Tabs b/share/html/Elements/Tabs
index c6c6505..136216a 100644
--- a/share/html/Elements/Tabs
+++ b/share/html/Elements/Tabs
@@ -936,6 +936,156 @@ my $build_main_nav = sub {
PageMenu()->child( edit => title => loc('Edit'), path => '/Prefs/MyRT.html' );
}
+ { ### START ASSETS MENU
+ # Top level Assets menu
+ my $assets = Menu->child("tools")->add_before(
+ "assets", title => loc("Assets"), path => "/Asset/Search/");
+ $assets->child("create", title => loc("Create"), path => "/Asset/CreateInCatalog.html");
+ $assets->child("search", title => loc("Search"), path => "/Asset/Search/");
+
+ Menu->child("search")->child("assets", title => loc("Assets"), path => "/Asset/Search/");
+
+ # Add global Assets custom field admin page
+ my $global_cfs = Menu();
+ $global_cfs = $global_cfs->child($_) or last
+ for "admin" => "global" => "custom-fields";
+ $global_cfs->child("assets", title => loc("Assets"), path => "/Admin/Global/CustomFields/Catalog-Assets.html")
+ if $global_cfs;
+
+ # Add a Catalog admin menu
+ my $config = Menu()->child("admin");
+ if ($config) {
+ my $assets = $config->child("tools")->add_before("assets", title => loc("Assets"), path => "/Admin/Assets/");
+ my $catalogs = $assets->child("catalogs",
+ title => loc("Catalogs"),
+ description => loc("Modify asset catalogs"),
+ path => "/Admin/Assets/Catalogs/");
+ $catalogs->child("select", title => loc("Select"), path => $catalogs->path);
+ $catalogs->child("create", title => loc("Create"), path => "Create.html");
+
+ my $cfs = $assets->child("cfs",
+ title => loc("Custom Fields"),
+ description => loc("Modify asset custom fields"),
+ path => "/Admin/CustomFields/?Type=" . RT::Asset->CustomFieldLookupType);
+ $cfs->child("select", title => loc("Select"), path => $cfs->path);
+ $cfs->child("create", title => loc("Create"), path => "/Admin/CustomFields/Modify.html?Create=1&LookupType=" . RT::Asset->CustomFieldLookupType);
+ }
+
+ # Asset search
+ if ($request_path =~ m{^/Asset/}) {
+ PageWidgets()->child( asset_search => raw_html => $m->scomp('/Elements/Assets/Search') );
+ PageWidgets()->delete('create_ticket');
+ PageWidgets()->delete('simple_search');
+ }
+
+ # Page menus
+ my $page = PageMenu();
+
+ if ($request_path =~ m{^/Asset/} and $DECODED_ARGS->{id} and $DECODED_ARGS->{id} !~ /\D/) {
+ my $id = $DECODED_ARGS->{id};
+ my $asset = RT::Asset->new( $session{CurrentUser} );
+ $asset->Load($id);
+
+ if ($asset->id) {
+ $page->child("display", title => loc("Display"), path => "/Asset/Display.html?id=$id");
+ $page->child("history", title => loc("History"), path => "/Asset/History.html?id=$id");
+ $page->child("basics", title => loc("Basics"), path => "/Asset/Modify.html?id=$id");
+ $page->child("links", title => loc("Links"), path => "/Asset/ModifyLinks.html?id=$id");
+ $page->child("people", title => loc("People"), path => "/Asset/ModifyPeople.html?id=$id");
+ $page->child("dates", title => loc("Dates"), path => "/Asset/ModifyDates.html?id=$id");
+
+ for my $grouping (RT::CustomField->CustomGroupings($asset)) {
+ my $cfs = $asset->CustomFields;
+ $cfs->LimitToGrouping( $asset => $grouping );
+ next unless $cfs->Count;
+ $page->child(
+ "cf-grouping-$grouping",
+ title => loc($grouping),
+ path => "/Asset/ModifyCFs.html?id=$id;Grouping=" . $m->interp->apply_escapes($grouping, 'u'),
+ );
+ }
+
+ my $actions = $page->child("actions", title => loc("Actions"));
+ $actions->child("create-linked-ticket", title => loc("Create linked ticket"), path => "/Asset/CreateLinkedTicket.html?Asset=$id");
+
+ my $status = $asset->Status;
+ my $lifecycle = $asset->LifecycleObj;
+ for my $action ( $lifecycle->Actions($status) ) {
+ my $next = $action->{'to'};
+ next unless $lifecycle->IsTransition( $status => $next );
+
+ my $check = $lifecycle->CheckRight( $status => $next );
+ next unless $asset->CurrentUserHasRight($check);
+
+ my $label = $action->{'label'} || ucfirst($next);
+ $actions->child(
+ $label,
+ title => loc($label),
+ path => "/Asset/Modify.html?id=$id;Update=1;DisplayAfter=1;Status="
+ . $m->interp->apply_escapes($next, 'u'),
+
+ class => "asset-lifecycle-action",
+ attributes => {
+ 'data-current-status' => $status,
+ 'data-next-status' => $next,
+ },
+ );
+ }
+ }
+ }
+ elsif ($request_path =~ m{^/Asset/Search/}) {
+ my %search = map @{$_},
+ grep defined $_->[1] && length $_->[1],
+ map {ref $DECODED_ARGS->{$_} ? [$_, $DECODED_ARGS->{$_}[0]] : [$_, $DECODED_ARGS->{$_}] }
+ grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
+ keys %$DECODED_ARGS;
+ if ( $request_path =~ /Bulk/) {
+ $page->child('search',
+ title => loc('Show Results'),
+ path => '/Asset/Search/?'. $query_string->(%search),
+ );
+ } else {
+ $page->child('bulk',
+ title => loc('Bulk Update'),
+ path => '/Asset/Search/Bulk.html?'. $query_string->(%search),
+ );
+ }
+ $page->child('csv',
+ title => loc('Download Spreadsheet'),
+ path => '/Asset/Search/Results.tsv?' . $query_string->(%search),
+ );
+ }
+ elsif ($request_path =~ m{^/Admin/Global/CustomFields/Catalog-Assets\.html$}) {
+ $page->child("create", title => loc("Create New"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
+ }
+ elsif ($request_path =~ m{^/Admin/CustomFields(/|/index\.html)?$}
+ and $DECODED_ARGS->{'Type'} and $DECODED_ARGS->{'Type'} eq RT::Asset->CustomFieldLookupType) {
+ $page->child("create")->path( $page->child("create")->path . "&LookupType=" . RT::Asset->CustomFieldLookupType );
+ }
+ elsif ($request_path =~ m{^/Admin/Assets/Catalogs/}) {
+ my $actions = $request_path =~ m{/((index|Create)\.html)?$}
+ ? $page
+ : $page->child("catalogs", title => loc("Catalogs"), path => "/Admin/Assets/Catalogs/");
+
+ $actions->child("select", title => loc("Select"), path => "/Admin/Assets/Catalogs/");
+ $actions->child("create", title => loc("Create"), path => "/Admin/Assets/Catalogs/Create.html");
+
+ my $catalog = RT::Catalog->new( $session{CurrentUser} );
+ $catalog->Load($DECODED_ARGS->{id}) if $DECODED_ARGS->{id};
+
+ if ($catalog->id and $catalog->CurrentUserCanSee) {
+ my $query = "id=" . $catalog->id;
+ $page->child("modify", title => loc("Basics"), path => "/Admin/Assets/Catalogs/Modify.html?$query");
+ $page->child("people", title => loc("Roles"), path => "/Admin/Assets/Catalogs/Roles.html?$query");
+
+ $page->child("cfs", title => loc("Asset Custom Fields"), path => "/Admin/Assets/Catalogs/CustomFields.html?$query");
+
+ $page->child("group-rights", title => loc("Group Rights"), path => "/Admin/Assets/Catalogs/GroupRights.html?$query");
+ $page->child("user-rights", title => loc("User Rights"), path => "/Admin/Assets/Catalogs/UserRights.html?$query");
+ }
+ }
+ } ### END ASSETS MENU
+
$m->callback( CallbackName => 'Privileged', Path => $request_path );
};
@@ -989,6 +1139,28 @@ my $build_selfservice_nav = sub {
PageWidgets->child( goto => raw_html => $m->scomp('/SelfService/Elements/GotoTicket') );
+ { ### START ASSETS SELFSERVICE MENU
+ Menu->child("tickets")->add_after(
+ "assets",
+ title => loc("Assets"),
+ path => "/SelfService/Asset/",
+ );
+
+ # Page menus
+ my $page = PageMenu();
+
+ if ($request_path =~ m{^/SelfService/Asset/} and $DECODED_ARGS->{id}) {
+ my $id = $DECODED_ARGS->{id};
+ $page->child("display", title => loc("Display"), path => "/SelfService/Asset/Display.html?id=$id");
+ $page->child("history", title => loc("History"), path => "/SelfService/Asset/History.html?id=$id");
+
+ if (Menu->child("new")) {
+ my $actions = $page->child("actions", title => loc("Actions"));
+ $actions->child("create-linked-ticket", title => loc("Create linked ticket"), path => "/SelfService/Asset/CreateLinkedTicket.html?Asset=$id");
+ }
+ }
+ } ### END ASSETS SELFSERVICE MENU
+
$m->callback( CallbackName => 'SelfService', Path => $request_path );
};
diff --git a/share/html/SelfService/Asset/CreateLinkedTicket.html b/share/html/SelfService/Asset/CreateLinkedTicket.html
new file mode 100644
index 0000000..ed4b8fd
--- /dev/null
+++ b/share/html/SelfService/Asset/CreateLinkedTicket.html
@@ -0,0 +1,57 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$id => undef
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+</%init>
+<& /Elements/Header,
+ Title => loc("Create linked ticket for asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+<& /Elements/Tabs &>
+<& /Asset/Elements/CreateLinkedTicket, AssetObj => $asset &>
diff --git a/share/html/SelfService/Asset/Display.html b/share/html/SelfService/Asset/Display.html
new file mode 100644
index 0000000..d105ced
--- /dev/null
+++ b/share/html/SelfService/Asset/Display.html
@@ -0,0 +1,57 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /SelfService/Elements/Header, Title => loc("Asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+
+<& /Asset/Elements/ShowSummary, AssetObj => $asset &>
+
+<%args>
+$id => undef
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+</%init>
diff --git a/share/html/SelfService/Asset/Helpers/CreateLinkedTicket b/share/html/SelfService/Asset/Helpers/CreateLinkedTicket
new file mode 100644
index 0000000..6cc25f5
--- /dev/null
+++ b/share/html/SelfService/Asset/Helpers/CreateLinkedTicket
@@ -0,0 +1,55 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Asset
+</%args>
+<%init>
+my $asset = LoadAsset($Asset);
+</%init>
+<& /Asset/Elements/CreateLinkedTicket, AssetObj => $asset &>
+% $m->abort;
diff --git a/share/html/SelfService/Asset/History.html b/share/html/SelfService/Asset/History.html
new file mode 100644
index 0000000..02f82e7
--- /dev/null
+++ b/share/html/SelfService/Asset/History.html
@@ -0,0 +1,60 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /SelfService/Elements/Header, Title => loc("History of Asset #[_1]: [_2]", $asset->id, $asset->Name) &>
+
+<& /Elements/ShowHistory,
+ Object => $asset,
+ ShowDisplayModes => 0,
+ &>
+
+<%args>
+$id => undef
+</%args>
+<%init>
+my $asset = LoadAsset($id);
+</%init>
diff --git a/share/html/SelfService/Asset/index.html b/share/html/SelfService/Asset/index.html
new file mode 100644
index 0000000..79016a8
--- /dev/null
+++ b/share/html/SelfService/Asset/index.html
@@ -0,0 +1,49 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /SelfService/Elements/Header, Title => loc("My Assets") &>
+<& /User/Elements/AssetList, User => $session{'CurrentUser'}->UserObj, Roles => [''], Title => loc('My Assets') &>
diff --git a/share/html/SelfService/Display.html b/share/html/SelfService/Display.html
index 9a115eb..584dea2 100644
--- a/share/html/SelfService/Display.html
+++ b/share/html/SelfService/Display.html
@@ -80,6 +80,8 @@
</tr>
</table>
+<& /Ticket/Elements/ShowAssets, Ticket => $Ticket &>
+
% $m->callback(CallbackName => 'BeforeShowHistory', ARGSRef=> \%ARGS, Ticket => $Ticket );
<& /Elements/ShowHistory,
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index b0e88b0..eb43a2e 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -111,6 +111,9 @@
<& /Ticket/Elements/EditTransactionCustomFields, %ARGS, QueueObj => $QueueObj, InTable => 1, KeepValue => 1, &>
</table>
</&>
+
+<& /Ticket/Elements/ShowAssetsOnCreate, QueueObj => $QueueObj, ARGSRef => \%ARGS &>
+
% $m->callback( CallbackName => 'AfterBasics', QueueObj => $QueueObj, ARGSRef => \%ARGS );
<& /Elements/EditCustomFieldCustomGroupings,
diff --git a/share/html/Ticket/Elements/ShowAssets b/share/html/Ticket/Elements/ShowAssets
new file mode 100644
index 0000000..7df9bb0
--- /dev/null
+++ b/share/html/Ticket/Elements/ShowAssets
@@ -0,0 +1,204 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Ticket
+$ShowRelatedTickets => 10
+</%args>
+<%init>
+my $target_assets = $Ticket->Links("Base")->Clone;
+$target_assets->Limit(
+ FIELD => "Target",
+ OPERATOR => "STARTSWITH",
+ VALUE => RT::URI::asset->LocalURIPrefix,
+);
+my $base_assets = $Ticket->Links("Target")->Clone;
+$base_assets->Limit(
+ FIELD => "Base",
+ OPERATOR => "STARTSWITH",
+ VALUE => RT::URI::asset->LocalURIPrefix,
+);
+
+my @linked_assets;
+push @linked_assets, grep { defined } map { $_->TargetURI->IsLocal }
+ @{ $target_assets->ItemsArrayRef };
+push @linked_assets, grep { defined } map { $_->BaseURI->IsLocal }
+ @{ $base_assets->ItemsArrayRef };
+
+my $asset_queue;
+if (RT->Config->Get('AssetQueues')) {
+ $asset_queue = 1 if grep {$_ eq $Ticket->QueueObj->Name} @{RT->Config->Get('AssetQueues')}
+} else {
+ $asset_queue = 1;
+}
+return unless @linked_assets or ($Ticket->CurrentUserHasRight("ModifyTicket")
+ and $asset_queue);
+
+my $assets = RT::Assets->new( $session{CurrentUser} );
+$assets->OrderBy( FIELD => "Name", ORDER => "ASC" );
+if ( @linked_assets ) {
+ $assets->Limit(
+ FIELD => "id",
+ OPERATOR => "IN",
+ VALUE => \@linked_assets,
+ );
+}
+
+my $Format = RT->Config->Get("AssetSummaryFormat") || q[
+ '<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
+ Description,
+ Status,
+ Catalog,
+];
+
+$m->callback(
+ CallbackName => 'ModifyCollection',
+ Ticket => $Ticket,
+ Assets => $assets,
+ Format => \$Format,
+);
+
+
+</%init>
+<&| /Widgets/TitleBox,
+ title => loc('Assets'),
+ class => 'ticket-assets',
+ title_class => "inverse",
+ &>
+
+<form action="<% RT->Config->Get("WebPath") %>/Ticket/Display.html" method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="id" value="<% $Ticket->id %>">
+
+% $m->callback( CallbackName => "Start", Ticket => $Ticket, Assets => $assets );
+
+<div id="assets-accordion" class="rt-accordion">
+% my $display_path = $session{'CurrentUser'}->Privileged ? 'Asset' : 'SelfService/Asset';
+% while (my $asset = $assets->Next) {
+ <h3><a href="<% RT->Config->Get('WebPath') %>/<% $display_path %>/Display.html?id=<% $asset->id %>"><&|/l, $asset->id, $asset->Name &>#[_1]: [_2]</&></a>
+<%perl>
+if ($Ticket->CurrentUserHasRight("ModifyTicket")) {
+ my $targets = $asset->Links("Target")->Clone;
+ $targets->Limit(
+ FIELD => "LocalBase",
+ VALUE => $Ticket->id,
+ );
+ my $bases = $asset->Links("Base")->Clone;
+ $bases->Limit(
+ FIELD => "LocalTarget",
+ VALUE => $Ticket->id,
+ );
+
+ my %params;
+ $params{join("-", "DeleteLink", "", $_->Type, $_->Target)} = 1
+ for @{ $targets->ItemsArrayRef };
+ $params{join("-", "DeleteLink", $_->Base, $_->Type, "")} = 1
+ for @{ $bases->ItemsArrayRef };
+
+ my $delete_url = RT->Config->Get("WebPath")
+ . "/Ticket/Display.html?"
+ . $m->comp("/Elements/QueryString", id => $Ticket->id, %params);
+</%perl>
+<a href="<% $delete_url %>" class="unlink-asset ui-icon ui-icon-circle-close" title="Unlink asset">X</a>
+% }
+ </h3>
+ <div class="details">
+ <& /Elements/ShowRecord,
+ Object => $asset,
+ Format => $Format,
+ TrustFormat => 1,
+ &>
+% $m->callback( CallbackName => "BeforeTickets", Ticket => $Ticket, Asset => $asset );
+<%perl>
+if ($ShowRelatedTickets) {
+ my %search = (
+ Query => "id != '@{[$Ticket->id]}' AND LinkedTo = 'asset:@{[$asset->id]}'",
+ OrderBy => "LastUpdated",
+ Order => "DESC",
+ );
+ my $url = RT->Config->Get("WebPath")
+ . "/Search/Results.html?"
+ . $m->comp("/Elements/QueryString", %search);
+</%perl>
+ <div class="related-tickets">
+ <span class="label">
+ <a href="<% $url %>">
+ <&|/l, $ShowRelatedTickets &>[_1] most recently updated related tickets</&>
+ </a>
+ </span>
+ <& /Elements/CollectionList,
+ %search,
+ Class => "RT::Tickets",
+ Format => RT->Config->Get("AssetSummaryRelatedTicketsFormat"),
+ Rows => $ShowRelatedTickets,
+ ShowHeader => 0,
+ AllowSorting => 0,
+ ShowNavigation => 0,
+ &>
+ </div>
+% }
+
+% $m->callback( CallbackName => "PerAsset", Ticket => $Ticket, Asset => $asset );
+
+ </div>
+% }
+</div>
+
+% if ($Ticket->CurrentUserHasRight("ModifyTicket")) {
+ <div class="add-asset">
+ <label>
+ <&|/l&>Add an asset to this ticket</&>
+ <input size="5" name="<% $Ticket->id %>-RefersTo" placeholder="<&|/l&>Asset #</&>" type="text">
+ </label>
+ <input type="submit" value="+">
+ </div>
+% }
+
+% $m->callback( CallbackName => "End", Ticket => $Ticket, Assets => $assets );
+
+</form>
+
+</&>
diff --git a/share/html/Ticket/Elements/ShowAssetsOnCreate b/share/html/Ticket/Elements/ShowAssetsOnCreate
new file mode 100644
index 0000000..a6f3bb4
--- /dev/null
+++ b/share/html/Ticket/Elements/ShowAssetsOnCreate
@@ -0,0 +1,121 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$QueueObj
+$ARGSRef
+</%args>
+<%init>
+my @linked_assets;
+foreach my $key ( map {+("new-$_", "$_-new")} keys %RT::Link::DIRMAP ) {
+ next unless $ARGSRef->{$key};
+ for my $linktext (grep $_, split ' ', $ARGSRef->{$key}) {
+ my $uri = RT::URI->new( $session{'CurrentUser'} );
+ next unless $uri->FromURI( $linktext );
+ next unless $uri->IsLocal and $uri->Object and $uri->Object->id and $uri->Object->isa("RT::Asset");
+ push @linked_assets, $uri->Object->id;
+ }
+}
+
+my $asset_queue;
+if (RT->Config->Get('AssetQueues')) {
+ $asset_queue = 1 if grep {$_ eq $QueueObj->Name} @{RT->Config->Get('AssetQueues')}
+} else {
+ $asset_queue = 1;
+}
+
+return unless @linked_assets or $asset_queue;
+
+my $assets = RT::Assets->new( $session{CurrentUser} );
+$assets->OrderBy( FIELD => "Name", ORDER => "ASC" );
+if ( @linked_assets ) {
+ $assets->Limit(
+ FIELD => "id",
+ OPERATOR => "IN",
+ VALUE => \@linked_assets,
+ );
+}
+
+my $Format = RT->Config->Get("AssetSummaryFormat") || q[
+ '<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
+ Description,
+ Status,
+ Catalog,
+];
+
+$m->callback(
+ CallbackName => 'ModifyCollection',
+ Queue => $QueueObj,
+ Assets => $assets,
+ Format => \$Format,
+);
+
+
+</%init>
+<&| /Widgets/TitleBox,
+ title => loc('Assets'),
+ class => 'ticket-assets',
+ title_class => "inverse",
+ &>
+
+% $m->callback( CallbackName => "Start", Queue => $QueueObj, Assets => $assets );
+
+<div id="assets-accordion" class="rt-accordion">
+% while (my $asset = $assets->Next) {
+ <h3><a href="<% RT->Config->Get('WebPath') %>/Asset/Display.html?id=<% $asset->id %>"><&|/l, $asset->id, $asset->Name &>#[_1]: [_2]</&></a></h3>
+ <div class="details">
+ <& /Elements/ShowRecord,
+ Object => $asset,
+ Format => $Format,
+ TrustFormat => 1,
+ &>
+% $m->callback( CallbackName => "PerAsset", Queue => $QueueObj, Asset => $asset );
+ </div>
+% }
+</div>
+% $m->callback( CallbackName => "End", Queue => $QueueObj, Assets => $assets );
+</&>
diff --git a/share/html/Ticket/Elements/ShowSummary b/share/html/Ticket/Elements/ShowSummary
index 36ddb20..b55064d 100644
--- a/share/html/Ticket/Elements/ShowSummary
+++ b/share/html/Ticket/Elements/ShowSummary
@@ -91,6 +91,7 @@
% $m->callback( %ARGS, CallbackName => 'AfterDates' );
% my (@extra);
% push @extra, titleright_raw => '<a href="'. RT->Config->Get('WebPath'). '/Ticket/Graphs/index.html?id='.$Ticket->id.'">'.loc('Graph').'</a>' unless RT->Config->Get('DisableGraphViz');
+<& /Ticket/Elements/ShowAssets, Ticket => $Ticket &>
% $m->callback( %ARGS, CallbackName => 'LinksExtra', extra => \@extra );
<&| /Widgets/TitleBox, title => loc('Links'),
($can_modify ? (title_href => RT->Config->Get('WebPath')."/Ticket/ModifyLinks.html?id=".$Ticket->Id) : ()),
diff --git a/share/html/User/Elements/AssetList b/share/html/User/Elements/AssetList
new file mode 100644
index 0000000..ef5a6a9
--- /dev/null
+++ b/share/html/User/Elements/AssetList
@@ -0,0 +1,78 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%init>
+my $assets = RT::Assets->new($session{CurrentUser});
+$m->callback( CallbackName => 'ModifyAssetSearch', %ARGS, Assets => $assets, Roles => \@Roles );
+for my $role (@Roles) {
+ $assets->RoleLimit(
+ TYPE => $role,
+ VALUE => $User->PrincipalId,
+ SUBCLAUSE => "Role$role",
+ );
+}
+my $Format = q[
+ '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
+ '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
+ Description,
+];
+$m->callback( CallbackName => 'ModifyFormat', %ARGS, Format => \$Format );
+</%init>
+<&| /Widgets/TitleBox, title => $Title, class => "user asset-list" &>
+ <& /Elements/CollectionList,
+ Collection => $assets,
+ OrderBy => 'id',
+ Order => 'ASC',
+ Format => $Format,
+ AllowSorting => 0,
+ &>
+</&>
+<%args>
+$User
+$Title
+ at Roles
+</%args>
diff --git a/share/html/User/Elements/Portlets/UserAssets b/share/html/User/Elements/Portlets/UserAssets
new file mode 100644
index 0000000..8bec6b1
--- /dev/null
+++ b/share/html/User/Elements/Portlets/UserAssets
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+%# Roles => [''] triggers the magical RoleLimit behavior that matches any role
+<& /User/Elements/AssetList, User => $User, Roles => [''], Title => loc('Assigned Assets') &>
+<%ARGS>
+$User
+</%ARGS>
diff --git a/share/static/css/base/assets.css b/share/static/css/base/assets.css
new file mode 100644
index 0000000..3526fa7
--- /dev/null
+++ b/share/static/css/base/assets.css
@@ -0,0 +1,229 @@
+#assets-accordion h3 {
+ position: relative;
+}
+
+#assets-accordion h3 a.unlink-asset {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: inherit;
+ padding: 0;
+}
+
+.ticket-assets .add-asset {
+ padding: 2em 0 0 0;
+ text-align: right;
+}
+
+body#comp-Asset-Search .collection-as-table td {
+ white-space: nowrap;
+}
+
+/* Colors */
+
+.asset-basics .titlebox .titlebox-title .left,
+.asset-info-cfs .titlebox .titlebox-title .left { background-color: #b32 }
+.asset-people .titlebox .titlebox-title .left { background-color: #48c }
+.asset-dates .titlebox .titlebox-title .left { background-color: #633063 }
+.asset-links .titlebox .titlebox-title .left { background-color: #316531 }
+.ticket-assets .titlebox .titlebox-title .left { background-color: #316531 }
+
+/* People display */
+
+#comp-Asset-Display .asset-people table {
+ width: 100%;
+}
+
+#comp-Asset-Display .asset-people td.label {
+ text-align: left;
+}
+
+#comp-Asset-Display .asset-people h3 {
+ margin: 0;
+ padding: 0;
+ line-height: 1.3;
+ font-size: 100%;
+}
+
+#comp-Asset-Display .asset-people .details {
+ padding: 0;
+}
+
+/* People editing */
+
+.asset-people .edit ul.role-members {
+ margin-top: 0;
+ padding-left: 0;
+ list-style: none;
+}
+
+.asset-people .edit td {
+ vertical-align: top;
+}
+
+.asset-people .edit .note {
+ font-size: 0.9em;
+}
+
+.asset-people .edit h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.asset-people .edit .role h3 {
+ margin-bottom: 0.5em;
+}
+
+.asset-people .edit .role,
+.asset-people .edit .add-group,
+.asset-people .edit .add-user {
+ margin-bottom: 1em;
+}
+
+/* Asset summary */
+
+.ticket-assets form {
+ display: inherit;
+}
+
+.ticket-assets .related-tickets {
+ margin-top: 1em;
+}
+
+.ticket-assets .related-tickets .label a {
+ font-weight: bold;
+ color: black;
+}
+
+.asset-metadata>div {
+ vertical-align: top;
+ min-width: 30%;
+ max-width: 30%;
+ padding-right: 1.5em;
+ display: inline-block;
+}
+
+.asset-metadata {
+ padding-top: 2em; /* nav overflows this :( */;
+}
+
+ at media (max-width: 800px) {
+ .asset-metadata>div {
+ min-width: 45%;
+ width: 45%;
+ }
+}
+
+/* on a little screen, let's just use a single column */
+ at media (max-width: 600px) {
+ .asset-metadata {
+ padding-top: 6em;
+ /* nav overflows this: < */;
+ }
+
+ .asset-metadata>div {
+ min-width: 100%;
+ width: 100%;
+ }
+
+ #Asset-Create-basics>table,
+ #Asset-Create-basics>table>tbody>tr,
+ #Asset-Create-basics>table>tbody>tr>td {
+ display: block;
+ }
+}
+
+/* On a reasonable-width screen, make better use of whitespace */
+ at media (min-width: 601px) {
+ .asset-info-cfs .edit-custom-fields {
+ width: 100%;
+ }
+
+ .asset-info-cfs .edit-custom-fields tr td,
+ #ModifyAsset .asset-basics tr td {
+ display: inline-block;
+ }
+
+ .asset-info-cfs .edit-custom-fields tr,
+ #ModifyAsset .asset-basics tr {
+ display: inline-block;
+ width: 49%;
+ }
+
+ .asset-info-cfs .edit-custom-fields tr td.cflabel,
+ #ModifyAsset .asset-basics tr td.label,
+ #ModifyAsset .asset-basics tr td.cflabel {
+ width: 8em;
+ }
+
+ /* Asset creation */
+ #Asset-Create-basics>table {
+ width: 100%;
+ align: left;
+ }
+
+ #Asset-Create-basics>table>tbody>tr>td {
+ padding-right: 2em;
+ }
+
+ #Asset-Create-basics>table>tbody>tr {
+ vertical-align: top;
+ }
+}
+
+/* basic cleanups for the search UI's elements */
+.asset-search-grouping input.datepicker {
+ width: 7em;
+}
+
+.asset-search-grouping td * {
+ max-width: 11em;
+}
+
+.asset-search-grouping td.label.not {
+ min-width: 3em;
+ width: auto;
+ padding-left: 1em;
+}
+
+/* On a wide screen, use two columns for search/bulk criteria */
+ at media (min-width:1150px) {
+ .asset-bulk-grouping.asset-bulk-cfs,
+ .asset-search-grouping.asset-search-cfs {
+ display: inline-block;
+ width: 45%;
+ padding-right: 1em;
+ vertical-align: top;
+ }
+
+ .titlebox.asset-bulk-grouping.asset-bulk-cfs,
+ .titlebox.asset-search-grouping.asset-search-cfs {
+ display: block;
+ width: auto;
+ padding: inherit;
+ }
+
+ .asset-bulk-people tr.full-width,
+ .asset-search-people tr.ful-width,
+ .asset-bulk-basics tr.full-width,
+ .asset-search-basics tr.full-width {
+ width: 100%;
+ }
+
+ .asset-bulk-people tr,
+ .asset-search-people tr,
+ .asset-bulk-basics tr,
+ .asset-search-basics tr {
+ width: 49%;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ .asset-bulk-people tr>td,
+ .asset-search-people tr>td,
+ .asset-bulk-basics tr>td,
+ .asset-search-basics tr>td {
+ display: inline-block;
+ width: 10em;
+ }
+}
diff --git a/share/static/js/assets.js b/share/static/js/assets.js
new file mode 100644
index 0000000..853ba86
--- /dev/null
+++ b/share/static/js/assets.js
@@ -0,0 +1,44 @@
+jQuery(function() {
+ var showModal = function(html) {
+ jQuery("<div class='modal'></div>")
+ .append(html).appendTo("body")
+ .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+ .modal();
+ };
+
+ var assets = jQuery("#assets-accordion");
+ assets.accordion({
+ // Open the accordion if there's only one fold, otherwise start with
+ // all assets collapsed.
+ active: assets.find("h3").length == 1 ? 0 : false,
+ collapsible: true,
+ heightStyle: 'content',
+ header: "h3"
+ }).find("h3 a.unlink-asset").click(function(ev){
+ ev.stopPropagation();
+ return true;
+ });
+ jQuery(".ticket-assets form").submit(function(){
+ var input = jQuery("[name*=RefersTo]", this);
+ if (input.val())
+ input.val(input.val().match(/\S+/g)
+ .map(function(x){return "asset:"+x})
+ .join(" "));
+ });
+ jQuery("#page-actions-create-linked-ticket").click(function(ev){
+ ev.preventDefault();
+ var url = this.href.replace(/\/Asset\/CreateLinkedTicket\.html\?/g,
+ '/Asset/Helpers/CreateLinkedTicket?');
+ jQuery.get(
+ url,
+ showModal
+ );
+ });
+ jQuery("#assets-create").click(function(ev){
+ ev.preventDefault();
+ jQuery.get(
+ RT.Config.WebHomePath + "/Asset/Helpers/CreateInCatalog",
+ showModal
+ );
+ });
+});
diff --git a/t/assets/api.t b/t/assets/api.t
new file mode 100644
index 0000000..df64eab
--- /dev/null
+++ b/t/assets/api.t
@@ -0,0 +1,179 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+use Test::Warn;
+
+my $catalog;
+
+diag "Create a catalog";
+{
+ $catalog = create_catalog( Name => 'Test Catalog', Disabled => 1 );
+ ok $catalog && $catalog->id, "Created catalog";
+ is $catalog->Name, "Test Catalog", "Name is correct";
+ ok $catalog->Disabled, "Disabled";
+
+ my $asset;
+ warning_like {
+ $asset = create_asset( Name => "Test", Catalog => $catalog->id );
+ } qr/^Failed to create asset .* Invalid catalog/i;
+ ok !$asset, "Couldn't create asset in disabled catalog";
+
+ my ($ok, $msg) = $catalog->SetDisabled(0);
+ ok $ok, "Enabled catalog: $msg";
+ ok !$catalog->Disabled, "Enabled";
+}
+
+diag "Create basic asset (no CFs)";
+{
+ my $asset = RT::Asset->new( RT->SystemUser );
+ my ($id, $msg) = $asset->Create(
+ Name => 'Thinkpad T420s',
+ Description => 'Laptop',
+ Catalog => $catalog->Name,
+ );
+ ok $id, "Created: $msg";
+ is $asset->id, $id, "id matches";
+ is $asset->Name, "Thinkpad T420s", "Name matches";
+ is $asset->Description, "Laptop", "Description matches";
+
+ # Create txn
+ my @txns = @{$asset->Transactions->ItemsArrayRef};
+ is scalar @txns, 1, "One transaction";
+ is $txns[0]->Type, "Create", "... of type Create";
+
+ # Update
+ my ($txnid, $txnmsg) = $asset->SetName("Lenovo Thinkpad T420s");
+ ok $txnid, "Updated Name: $txnmsg";
+ is $asset->Name, "Lenovo Thinkpad T420s", "New Name matches";
+
+ # Set txn
+ @txns = @{$asset->Transactions->ItemsArrayRef};
+ is scalar @txns, 2, "Two transactions";
+ is $txns[1]->Type, "Set", "... the second of which is Set";
+ is $txns[1]->Field, "Name", "... Field is Name";
+ is $txns[1]->OldValue, "Thinkpad T420s", "... OldValue is correct";
+
+ # Delete
+ my ($ok, $err) = $asset->Delete;
+ ok !$ok, "Deletes are prevented: $err";
+ $asset->Load($id);
+ ok $asset->id, "Asset not deleted";
+}
+
+diag "Create with CFs";
+{
+ my $height = create_cf( Name => 'Height' );
+ ok $height->id, "Created CF";
+
+ my $material = create_cf( Name => 'Material' );
+ ok $material->id, "Created CF";
+
+ ok apply_cfs($height, $material), "Applied CFs";
+
+ my $asset = RT::Asset->new( RT->SystemUser );
+ my ($id, $msg) = $asset->Create(
+ Name => 'Standing desk',
+ "CustomField-".$height->id => '46"',
+ "CustomField-Material" => 'pine',
+ Catalog => $catalog->Name,
+ );
+ ok $id, "Created: $msg";
+ is $asset->FirstCustomFieldValue('Height'), '46"', "Found height";
+ is $asset->FirstCustomFieldValue('Material'), 'pine', "Found material";
+ is $asset->Transactions->Count, 1, "Only a single txn";
+}
+
+note "Create/update with Roles";
+{
+ my $root = RT::User->new( RT->SystemUser );
+ $root->Load("root");
+ ok $root->id, "Found root";
+
+ my $bps = RT::Test->load_or_create_user( Name => "BPS" );
+ ok $bps->id, "Created BPS user";
+
+ my $asset = RT::Asset->new( RT->SystemUser );
+ my ($id, $msg) = $asset->Create(
+ Name => 'RT server',
+ HeldBy => $root->PrincipalId,
+ Owner => $bps->PrincipalId,
+ Contact => $bps->PrincipalId,
+ Catalog => $catalog->id,
+ );
+ ok $id, "Created: $msg";
+ is $asset->HeldBy->UserMembersObj->First->Name, "root", "root is Holder";
+ is $asset->Owner->Name, "BPS", "BPS is Owner";
+ is $asset->Contacts->UserMembersObj->First->Name, "BPS", "BPS is Contact";
+
+ my $sysadmins = RT::Group->new( RT->SystemUser );
+ $sysadmins->CreateUserDefinedGroup( Name => 'Sysadmins' );
+ ok $sysadmins->id, "Created group";
+ is $sysadmins->Name, "Sysadmins", "Got group name";
+
+ (my $ok, $msg) = $asset->AddRoleMember(
+ Type => 'Contact',
+ Group => 'Sysadmins',
+ );
+ ok $ok, "Added Sysadmins as Contact: $msg";
+ is $asset->Contacts->MembersObj->Count, 2, "Found two members";
+
+ my @txn = grep { $_->Type eq 'AddWatcher' } @{$asset->Transactions->ItemsArrayRef};
+ ok @txn == 1, "Found one AddWatcher txn";
+ is $txn[0]->Field, "Contact", "... of a Contact";
+ is $txn[0]->NewValue, $sysadmins->PrincipalId, "... for the right principal";
+
+ ($ok, $msg) = $asset->DeleteRoleMember(
+ Type => 'Contact',
+ PrincipalId => $bps->PrincipalId,
+ );
+ ok $ok, "Removed BPS user as Contact: $msg";
+ is $asset->Contacts->MembersObj->Count, 1, "Now just one member";
+ is $asset->Contacts->GroupMembersObj(Recursively => 0)->First->Name, "Sysadmins", "... it's Sysadmins";
+
+ @txn = grep { $_->Type eq 'DelWatcher' } @{$asset->Transactions->ItemsArrayRef};
+ ok @txn == 1, "Found one DelWatcher txn";
+ is $txn[0]->Field, "Contact", "... of a Contact";
+ is $txn[0]->OldValue, $bps->PrincipalId, "... for the right principal";
+}
+
+diag "Custom Field handling";
+{
+ diag "Make sure we don't load queue CFs";
+ my $queue_cf = RT::CustomField->new( RT->SystemUser );
+ my ($ok, $msg) = $queue_cf->Create(
+ Name => "Queue CF",
+ Type => "Text",
+ LookupType => RT::Queue->CustomFieldLookupType,
+ );
+ ok( $queue_cf->Id, "Created test CF: " . $queue_cf->Id);
+
+ my $cf1 = RT::CustomField->new( RT->SystemUser );
+ $cf1->LoadByNameAndCatalog ( Name => "Queue CF" );
+
+ ok( (not $cf1->Id), "Queue CF not loaded with LoadByNameAndCatalog");
+
+ my $cf2 = RT::CustomField->new( RT->SystemUser );
+ $cf2->LoadByNameAndCatalog ( Name => "Height" );
+ ok( $cf2->Id, "Loaded CF id: " . $cf2->Id . " with name");
+ ok( $cf2->Name, "Loaded CF name: " . $cf2->Name . " with name");
+
+ my $cf3 = RT::CustomField->new( RT->SystemUser );
+ ($ok, $msg) = $cf3->LoadByNameAndCatalog ( Name => "Height", Catalog => $catalog->Name );
+ ok( (not $cf3->Id), "CF 'Height'"
+ . " not added to catalog: " . $catalog->Name);
+
+ my $color = create_cf( Name => 'Color' );
+ ok $color->Id, "Created CF " . $color->Name;
+ ($ok, $msg) = $color->AddToObject( $catalog );
+
+ ($ok, $msg) = $color->LoadByNameAndCatalog ( Name => "Color", Catalog => $catalog->Name );
+ ok( $color->Id, "Loaded CF id: " . $color->Id
+ . " for catalog: " . $catalog->Name);
+ ok( $color->Name, "Loaded CF name: " . $color->Name
+ . " for catalog: " . $catalog->Name);
+
+}
+
+
+done_testing;
diff --git a/t/assets/collection.t b/t/assets/collection.t
new file mode 100644
index 0000000..f2c3019
--- /dev/null
+++ b/t/assets/collection.t
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+
+my $user = RT::Test->load_or_create_user( Name => 'testuser' );
+ok $user->id, "Created user";
+
+my $catalog = create_catalog( Name => "BPS" );
+ok $catalog && $catalog->id, "Created catalog";
+
+my $location = create_cf( Name => 'Location' );
+ok $location->id, "Created CF";
+ok apply_cfs($location), "Applied CF";
+
+ok(
+ create_assets(
+ { Name => "Thinkpad T420s", Catalog => $catalog->id, "CustomField-Location" => "Home" },
+ { Name => "Standing desk", Catalog => $catalog->id, "CustomField-Location" => "Office" },
+ { Name => "Chair", Catalog => $catalog->id, "CustomField-Location" => "Office" },
+ ),
+ "Created assets"
+);
+
+diag "Mark chair as deleted";
+{
+ my $asset = RT::Asset->new( RT->SystemUser );
+ $asset->LoadByCols( Name => "Chair" );
+ my ($ok, $msg) = $asset->SetStatus( "deleted" );
+ ok($ok, "Deleted the chair: $msg");
+}
+
+diag "Basic types of limits";
+{
+ my $assets = RT::Assets->new( RT->SystemUser );
+ $assets->Limit( FIELD => 'Name', OPERATOR => 'LIKE', VALUE => 'thinkpad' );
+ is $assets->Count, 1, "Found 1 like thinkpad";
+ is $assets->First->Name, "Thinkpad T420s";
+
+ $assets = RT::Assets->new( RT->SystemUser );
+ $assets->UnLimit;
+ is $assets->Count, 2, "Found 2 total";
+ ok((!grep { $_->Name eq "Chair" } @{$assets->ItemsArrayRef}), "No chair (disabled)");
+
+ $assets = RT::Assets->new( RT->SystemUser );
+ $assets->Limit( FIELD => 'Status', VALUE => 'deleted' );
+ $assets->{allow_deleted_search} = 1;
+ is $assets->Count, 1, "Found 1 deleted";
+ is $assets->First->Name, "Chair", "Found chair";
+
+ $assets = RT::Assets->new( RT->SystemUser );
+ $assets->UnLimit;
+ $assets->LimitCustomField(
+ CUSTOMFIELD => $location->id,
+ VALUE => "Office",
+ );
+ is $assets->Count, 1, "Found 1 in Office";
+ ok $assets->First, "Got record";
+ is $assets->First->Name, "Standing desk", "Found standing desk";
+}
+
+diag "Test ACLs";
+{
+ my $assets = RT::Assets->new( RT::CurrentUser->new($user) );
+ $assets->UnLimit;
+ is scalar @{$assets->ItemsArrayRef}, 0, "Found none";
+}
+
+done_testing;
diff --git a/t/assets/compile.t b/t/assets/compile.t
new file mode 100644
index 0000000..ea27bc5
--- /dev/null
+++ b/t/assets/compile.t
@@ -0,0 +1,10 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use_ok('RT::Test::Assets');
+use_ok('RT::Asset');
+use_ok('RT::Assets');
+use_ok('RT::Catalog');
+use_ok('RT::Catalogs');
diff --git a/t/assets/links.t b/t/assets/links.t
new file mode 100644
index 0000000..a9101fe
--- /dev/null
+++ b/t/assets/links.t
@@ -0,0 +1,129 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+use Test::Warn;
+
+my $catalog = create_catalog( Name => "BPS" );
+ok $catalog && $catalog->id, "Created Catalog";
+
+ok(
+ create_assets(
+ { Name => "Thinkpad T420s", Catalog => $catalog->id },
+ { Name => "Standing desk", Catalog => $catalog->id },
+ { Name => "Chair", Catalog => $catalog->id },
+ ),
+ "Created assets"
+);
+
+my $ticket = RT::Test->create_ticket(
+ Queue => 1,
+ Subject => 'a test ticket',
+);
+ok $ticket->id, "Created ticket";
+
+diag "RT::URI::asset";
+{
+ my %uris = (
+ # URI => Asset Name
+ "asset:1" => { id => 1, Name => "Thinkpad T420s" },
+ "asset://example.com/2" => { id => 2, Name => "Standing desk" },
+ "asset:13" => undef,
+ );
+
+ while (my ($url, $expected) = each %uris) {
+ my $uri = RT::URI->new( RT->SystemUser );
+ if ($expected) {
+ my $parsed = $uri->FromURI($url);
+ ok $parsed, "Parsed $url";
+
+ my $asset = $uri->Object;
+ ok $asset, "Got object";
+ is ref($asset), "RT::Asset", "... it's a RT::Asset";
+
+ while (my ($field, $value) = each %$expected) {
+ is $asset->$field, $value, "... $field is $value";
+ }
+ } else {
+ my $parsed;
+ warnings_like {
+ $parsed = $uri->FromURI($url);
+ } [qr/\Q$url\E/, qr/\Q$url\E/], "Caught warnings about unknown URI";
+ ok !$parsed, "Failed to parse $url, as expected";
+ }
+ }
+}
+
+diag "RT::Asset link support";
+{
+ my $chair = RT::Asset->new( RT->SystemUser );
+ $chair->LoadByCols( Name => "Chair" );
+ ok $chair->id, "Loaded asset";
+ is $chair->URI, "asset://example.com/".$chair->id, "->URI works";
+
+ my ($link_id, $msg) = $chair->AddLink( Type => 'MemberOf', Target => 'asset:2' );
+ ok $link_id, "Added link: $msg";
+
+ my $parents = $chair->MemberOf;
+ my $desk = $parents->First->TargetObj;
+ is $parents->Count, 1, "1 parent";
+ is $desk->Name, "Standing desk", "Correct parent asset";
+
+ for my $asset ($chair, $desk) {
+ my $txns = $asset->Transactions;
+ $txns->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is $txns->Count, 1, "1 AddLink txn on asset ".$asset->Name;
+ }
+
+ my ($ok, $err) = $chair->DeleteLink( Type => 'MemberOf', Target => 'asset:1' );
+ ok !$ok, "Delete link failed on non-existent: $err";
+
+ my ($deleted, $delete_msg) = $chair->DeleteLink( Type => 'MemberOf', Target => $parents->First->Target );
+ ok $deleted, "Deleted link: $delete_msg";
+
+ for my $asset ($chair, $desk) {
+ my $txns = $asset->Transactions;
+ $txns->Limit( FIELD => 'Type', VALUE => 'DeleteLink' );
+ is $txns->Count, 1, "1 DeleteLink txn on asset ".$asset->Name;
+ }
+};
+
+diag "Linking to tickets";
+{
+ my $laptop = RT::Asset->new( RT->SystemUser );
+ $laptop->LoadByCols( Name => "Thinkpad T420s" );
+
+ my ($ok, $msg) = $ticket->AddLink( Type => 'RefersTo', Target => $laptop->URI );
+ ok $ok, "Ticket refers to asset: $msg";
+
+ my $links = $laptop->ReferredToBy;
+ is $links->Count, 1, "Found a ReferredToBy link via asset";
+
+ ($ok, $msg) = $laptop->DeleteLink( Type => 'RefersTo', Base => $ticket->URI );
+ ok $ok, "Deleted link from opposite side: $msg";
+}
+
+diag "Links on ->Create";
+{
+ my $desk = RT::Asset->new( RT->SystemUser );
+ $desk->LoadByCols( Name => "Standing desk" );
+ ok $desk->id, "Loaded standing desk asset";
+
+ my $asset = create_asset(
+ Name => "Anti-fatigue mat",
+ Catalog => $catalog->id,
+ Parent => $desk->URI,
+ ReferredToBy => [$ticket->id],
+ );
+ ok $asset->id, "Created asset with Parent link";
+
+ my $parents = $asset->MemberOf;
+ is $parents->Count, 1, "Found one Parent";
+ is $parents->First->Target, $desk->URI, "... it's a desk!";
+
+ my $referrals = $asset->ReferredToBy;
+ is $referrals->Count, 1, "Found one ReferredToBy";
+ is $referrals->First->Base, $ticket->URI, "... it's the ticket!";
+}
+
+done_testing;
diff --git a/t/assets/pod.t b/t/assets/pod.t
new file mode 100644
index 0000000..1d2686c
--- /dev/null
+++ b/t/assets/pod.t
@@ -0,0 +1,6 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Pod;
+all_pod_files_ok( all_pod_files("lib","doc","etc"));
diff --git a/t/assets/rights.t b/t/assets/rights.t
new file mode 100644
index 0000000..b28b16b
--- /dev/null
+++ b/t/assets/rights.t
@@ -0,0 +1,124 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+
+my $user = RT::Test->load_or_create_user( Name => 'testuser' );
+ok $user->id, "Created user";
+
+my $ticket = RT::Test->create_ticket(
+ Queue => 1,
+ Subject => 'a test ticket',
+);
+ok $ticket->id, "Created ticket";
+
+my $catalog_one = create_catalog( Name => "One" );
+ok $catalog_one && $catalog_one->id, "Created catalog one";
+
+my $catalog_two = create_catalog( Name => "Two" );
+ok $catalog_two && $catalog_two->id, "Created catalog two";
+
+ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'ShowCatalog',
+ Object => $catalog_one,
+}), "Granted ShowCatalog");
+
+my $asset = RT::Asset->new( RT::CurrentUser->new($user) );
+
+diag "CreateAsset";
+{
+ my %create = (
+ Name => 'Thinkpad T420s',
+ Contact => 'trs at example.com',
+ Catalog => $catalog_one->id,
+ );
+ my ($id, $msg) = $asset->Create(%create);
+ ok !$id, "Create denied: $msg";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'CreateAsset',
+ Object => $catalog_one,
+ }), "Granted CreateAsset");
+
+ ($id, $msg) = $asset->Create(%create);
+ ok $id, "Created: $msg";
+ is $asset->id, $id, "id matches";
+ is $asset->CatalogObj->Name, $catalog_one->Name, "Catalog matches";
+};
+
+diag "ShowAsset";
+{
+ is $asset->Name, undef, "Can't see Name without ShowAsset";
+ ok !$asset->Contacts->id, "Can't see Contacts role group";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'ShowAsset',
+ Object => $catalog_one,
+ }), "Granted ShowAsset");
+
+ is $asset->Name, "Thinkpad T420s", "Got Name";
+ is $asset->Contacts->UserMembersObj->First->EmailAddress, 'trs at example.com', "Got Contact";
+}
+
+diag "ModifyAsset";
+{
+ my ($txnid, $txnmsg) = $asset->SetName("Lenovo Thinkpad T420s");
+ ok !$txnid, "Update failed: $txnmsg";
+ is $asset->Name, "Thinkpad T420s", "Name didn't change";
+
+ my ($ok, $msg) = $asset->AddLink( Type => 'RefersTo', Target => 't:1' );
+ ok !$ok, "No rights to AddLink: $msg";
+
+ ($ok, $msg) = $asset->DeleteLink( Type => 'RefersTo', Target => 't:1' );
+ ok !$ok, "No rights to DeleteLink: $msg";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'ModifyAsset',
+ Object => $catalog_one,
+ }), "Granted ModifyAsset");
+
+ ($txnid, $txnmsg) = $asset->SetName("Lenovo Thinkpad T420s");
+ ok $txnid, "Updated Name: $txnmsg";
+ is $asset->Name, "Lenovo Thinkpad T420s", "Name changed";
+}
+
+diag "Catalogs";
+{
+ my ($txnid, $txnmsg) = $asset->SetCatalog($catalog_two->id);
+ ok !$txnid, "Failed to update Catalog: $txnmsg";
+ is $asset->CatalogObj->Name, $catalog_one->Name, "Catalog unchanged";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'CreateAsset',
+ Object => $catalog_two,
+ }), "Granted CreateAsset in second catalog");
+
+ ($txnid, $txnmsg) = $asset->SetCatalog($catalog_two->id);
+ ok $txnid, "Updated Catalog: $txnmsg";
+ unlike $txnmsg, qr/Permission Denied/i, "Transaction message isn't Permission Denied";
+ ok !$asset->CurrentUserCanSee, "Can no longer see the asset";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'ShowAsset',
+ Object => $catalog_two,
+ }), "Granted ShowAsset");
+
+ ok $asset->CurrentUserCanSee, "Can see the asset now";
+ is $asset->CatalogObj->Name, undef, "Can't see the catalog name still";
+
+ ok(RT::Test->add_rights({
+ Principal => 'Privileged',
+ Right => 'ShowCatalog',
+ Object => $catalog_two,
+ }), "Granted ShowCatalog");
+
+ is $asset->CatalogObj->Name, $catalog_two->Name, "Now we can see the catalog name";
+}
+
+done_testing;
diff --git a/t/assets/roles.t b/t/assets/roles.t
new file mode 100644
index 0000000..1d8a647
--- /dev/null
+++ b/t/assets/roles.t
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+
+my $catalog = create_catalog( Name => "A catalog" );
+my $asset = create_asset( Name => "Test asset", Catalog => $catalog->id );
+ok $asset && $asset->id, "Created asset";
+
+for my $object ($asset, $catalog, RT->System) {
+ for my $role (RT::Asset->Roles) {
+ my $group = $object->RoleGroup($role);
+ ok $group->id, "Loaded role group $role for " . ref($object);
+
+ my $principal = $group->PrincipalObj;
+ ok $principal && $principal->id, "Found PrincipalObj for role group"
+ or next;
+
+ if ($object->DOES("RT::Record::Role::Rights")) {
+ my ($ok, $msg) = $principal->GrantRight(
+ Object => $object,
+ Right => "ShowAsset",
+ );
+ ok $ok, "Granted right" or diag "Error: $msg";
+ }
+ }
+}
+
+done_testing;
diff --git a/t/assets/web.t b/t/assets/web.t
new file mode 100644
index 0000000..3595e26
--- /dev/null
+++ b/t/assets/web.t
@@ -0,0 +1,113 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+
+RT->Config->Set("CustomFieldGroupings",
+ "RT::Asset" => {
+ Dates => [qw(Purchased)],
+ },
+);
+
+my $catalog = create_catalog( Name => "Office" );
+ok $catalog->id, "Created Catalog";
+
+my $purchased = create_cf( Name => 'Purchased', Pattern => '(?#Year)^(?:19|20)\d{2}$' );
+ok $purchased->id, "Created CF";
+
+my $height = create_cf( Name => 'Height', Pattern => '(?#Inches)^\d+"?$' );
+ok $height->id, "Created CF";
+
+my $material = create_cf( Name => 'Material' );
+ok $material->id, "Created CF";
+
+my %CF = (
+ Height => ".CF-" . $height->id . "-Edit",
+ Material => ".CF-" . $material->id . "-Edit",
+ Purchased => ".CF-" . $purchased->id . "-Edit",
+);
+
+my ($base, $m) = RT::Test::Assets->started_ok;
+ok $m->login, "Logged in agent";
+
+diag "Create basic asset (no CFs)";
+{
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id } }, "Picked a catalog");
+ $m->submit_form_ok({
+ with_fields => {
+ id => 'new',
+ Name => 'Thinkpad T420s',
+ Description => 'A laptop',
+ },
+ }, "submited create form");
+ $m->content_like(qr/Asset .* created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+
+ my $asset = RT::Asset->new( RT->SystemUser );
+ $asset->Load($id);
+ is $asset->id, $id, "id matches";
+ is $asset->Name, "Thinkpad T420s", "Name matches";
+ is $asset->Description, "A laptop", "Description matches";
+}
+
+diag "Create with CFs";
+{
+ ok apply_cfs($height, $material), "Applied CFs";
+
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id } }, "Picked a catalog");
+
+ ok $m->form_with_fields(qw(id Name Description)), "Found form";
+ $m->submit_form_ok({
+ fields => {
+ id => 'new',
+ Name => 'Standing desk',
+ $CF{Height} => 'forty-six inches',
+ $CF{Material} => 'pine',
+ },
+ }, "submited create form");
+ $m->content_unlike(qr/Asset .* created/, "Lacks created message");
+ $m->content_like(qr/must match .*?Inches/, "Found validation error");
+
+ # Intentionally fix only the invalid CF to test the other fields are
+ # preserved across errors
+ ok $m->form_with_fields(qw(id Name Description)), "Found form again";
+ $m->set_fields( $CF{Height} => '46"' );
+ $m->submit_form_ok({}, "resubmitted form");
+
+ $m->content_like(qr/Asset .* created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+
+ my $asset = RT::Asset->new( RT->SystemUser );
+ $asset->Load($id);
+ is $asset->id, $id, "id matches";
+ is $asset->FirstCustomFieldValue('Height'), '46"', "Found height";
+ is $asset->FirstCustomFieldValue('Material'), 'pine', "Found material";
+}
+
+diag "Create with CFs in other groups";
+{
+ ok apply_cfs($purchased), "Applied CF";
+
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id } }, "Picked a catalog");
+
+ ok $m->form_with_fields(qw(id Name Description)), "Found form";
+
+ $m->submit_form_ok({
+ fields => {
+ id => 'new',
+ Name => 'Chair',
+ $CF{Height} => '23',
+ },
+ }, "submited create form");
+
+ $m->content_like(qr/Asset .* created/, "Found created message");
+ $m->content_unlike(qr/Purchased.*?must match .*?Year/, "Lacks validation error for Purchased");
+}
+
+# XXX TODO: test other modify pages
+
+undef $m;
+done_testing;
-----------------------------------------------------------------------
More information about the rt-commit
mailing list