Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/bestpractical/rt.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJim Brandt <jbrandt@bestpractical.com>2022-10-21 21:31:36 +0300
committerJim Brandt <jbrandt@bestpractical.com>2022-10-21 21:31:36 +0300
commitd740e2ca83ebef758296a5725a359ecf1fc75f65 (patch)
treebe9fe7b169d65e9540eaa12234ca9a609733137b
parentd52b11cb513a16ba7807435b8bc0257ce42ddf36 (diff)
parenta126fafd8b105f279a9b5d2829f0993790d63c6d (diff)
Merge branch '5.0/asset-custom-roles' into 5.0-trunk
-rw-r--r--etc/schema.Oracle1
-rw-r--r--etc/schema.Pg1
-rw-r--r--etc/schema.SQLite1
-rw-r--r--etc/schema.mysql1
-rw-r--r--etc/upgrade/5.0.4/schema.Oracle2
-rw-r--r--etc/upgrade/5.0.4/schema.Pg2
-rw-r--r--etc/upgrade/5.0.4/schema.SQLite2
-rw-r--r--etc/upgrade/5.0.4/schema.mysql2
-rw-r--r--lib/RT/Action/Notify.pm1
-rw-r--r--lib/RT/Asset.pm44
-rw-r--r--lib/RT/Assets.pm57
-rw-r--r--lib/RT/CustomField.pm157
-rw-r--r--lib/RT/CustomRole.pm250
-rw-r--r--lib/RT/CustomRoles.pm18
-rw-r--r--lib/RT/Group.pm2
-rw-r--r--lib/RT/Interface/Web.pm9
-rw-r--r--lib/RT/ObjectCustomRole.pm46
-rw-r--r--lib/RT/ObjectCustomRoles.pm19
-rw-r--r--lib/RT/Principal.pm4
-rw-r--r--lib/RT/Queue.pm2
-rw-r--r--lib/RT/Record/Role/LookupType.pm290
-rw-r--r--lib/RT/Record/Role/Roles.pm52
-rw-r--r--lib/RT/Ticket.pm65
-rw-r--r--lib/RT/Tickets.pm1
-rw-r--r--sbin/rt-validator.in2
-rw-r--r--share/html/Admin/CustomFields/Modify.html4
-rw-r--r--share/html/Admin/CustomRoles/Modify.html13
-rw-r--r--share/html/Admin/CustomRoles/Objects.html18
-rw-r--r--share/html/Admin/CustomRoles/index.html7
-rw-r--r--share/html/Admin/Elements/SelectCustomFieldLookupType17
-rw-r--r--share/html/Admin/Elements/SelectLookupType61
-rw-r--r--share/html/Asset/Create.html4
-rw-r--r--share/html/Asset/Elements/AssetSearchPeople4
-rw-r--r--share/html/Asset/Elements/EditCatalogPeople4
-rw-r--r--share/html/Asset/Elements/EditPeople13
-rw-r--r--share/html/Asset/Elements/EditRoleMembers9
-rw-r--r--share/html/Asset/Elements/SelectRoleType2
-rw-r--r--share/html/Asset/Elements/ShowPeople4
-rw-r--r--share/html/Asset/Elements/ShowRoleMembers2
-rw-r--r--share/html/Asset/Search/Bulk.html35
-rw-r--r--share/html/Elements/ColumnMap7
-rw-r--r--share/html/Elements/RT__CustomRole/ColumnMap11
-rw-r--r--share/html/Search/Bulk.html2
-rw-r--r--share/html/Search/Elements/BuildFormatString14
-rw-r--r--share/html/Search/Elements/PickBasics2
-rw-r--r--share/html/Search/Elements/PickCustomRoles29
-rw-r--r--share/html/Search/Elements/SelectPersonType24
-rw-r--r--t/customroles/assets.t330
-rw-r--r--t/customroles/web-assets.t279
49 files changed, 1573 insertions, 353 deletions
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 324f790d19..7b7ac07475 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -520,6 +520,7 @@ CREATE TABLE CustomRoles (
Description VARCHAR2(255),
MaxValues NUMBER(11,0) DEFAULT 0 NOT NULL,
EntryHint VARCHAR2(255),
+ LookupType VARCHAR2(255),
Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
Created DATE,
LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
diff --git a/etc/schema.Pg b/etc/schema.Pg
index 9f34ec4b0a..0c81859312 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -753,6 +753,7 @@ CREATE TABLE CustomRoles (
Description varchar(255) NULL ,
MaxValues integer NOT NULL DEFAULT 0 ,
EntryHint varchar(255) NULL ,
+ LookupType varchar(255) NOT NULL ,
Creator integer NOT NULL DEFAULT 0 ,
Created TIMESTAMP NULL ,
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index d2e455f9e5..2811ed2c4c 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -549,6 +549,7 @@ CREATE TABLE CustomRoles (
Description varchar(255) collate NOCASE NULL ,
MaxValues integer,
EntryHint varchar(255) collate NOCASE NULL ,
+ LookupType varchar(255) collate NOCASE NOT NULL,
Creator integer NOT NULL DEFAULT 0 ,
Created DATETIME NULL ,
diff --git a/etc/schema.mysql b/etc/schema.mysql
index f773ffd472..15868174dc 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -538,6 +538,7 @@ CREATE TABLE CustomRoles (
Description varchar(255) NULL ,
MaxValues integer,
EntryHint varchar(255) NULL ,
+ LookupType varchar(255) CHARACTER SET ascii NOT NULL,
Creator integer NOT NULL DEFAULT 0 ,
Created DATETIME NULL ,
diff --git a/etc/upgrade/5.0.4/schema.Oracle b/etc/upgrade/5.0.4/schema.Oracle
new file mode 100644
index 0000000000..300bf8d8b6
--- /dev/null
+++ b/etc/upgrade/5.0.4/schema.Oracle
@@ -0,0 +1,2 @@
+ALTER TABLE CustomRoles ADD LookupType VARCHAR2(255);
+UPDATE CustomRoles SET LookupType='RT::Queue-RT::Ticket';
diff --git a/etc/upgrade/5.0.4/schema.Pg b/etc/upgrade/5.0.4/schema.Pg
new file mode 100644
index 0000000000..671d871f45
--- /dev/null
+++ b/etc/upgrade/5.0.4/schema.Pg
@@ -0,0 +1,2 @@
+ALTER TABLE CustomRoles ADD COLUMN LookupType VARCHAR(255);
+UPDATE CustomRoles SET LookupType='RT::Queue-RT::Ticket';
diff --git a/etc/upgrade/5.0.4/schema.SQLite b/etc/upgrade/5.0.4/schema.SQLite
new file mode 100644
index 0000000000..ec766a33cd
--- /dev/null
+++ b/etc/upgrade/5.0.4/schema.SQLite
@@ -0,0 +1,2 @@
+ALTER TABLE CustomRoles ADD COLUMN LookupType VARCHAR(255) collate NOCASE;
+UPDATE CustomRoles SET LookupType='RT::Queue-RT::Ticket';
diff --git a/etc/upgrade/5.0.4/schema.mysql b/etc/upgrade/5.0.4/schema.mysql
new file mode 100644
index 0000000000..850f200953
--- /dev/null
+++ b/etc/upgrade/5.0.4/schema.mysql
@@ -0,0 +1,2 @@
+ALTER TABLE CustomRoles ADD COLUMN LookupType varchar(255) CHARACTER SET ascii;
+UPDATE CustomRoles SET LookupType='RT::Queue-RT::Ticket';
diff --git a/lib/RT/Action/Notify.pm b/lib/RT/Action/Notify.pm
index f6fcbc0654..e1ad1b4458 100644
--- a/lib/RT/Action/Notify.pm
+++ b/lib/RT/Action/Notify.pm
@@ -122,6 +122,7 @@ sub SetRecipients {
}
else {
my $roles = RT::CustomRoles->new( $self->CurrentUser );
+ $roles->LimitToLookupType( RT::Ticket->CustomFieldLookupType );
$roles->Limit( FIELD => 'Name', VALUE => $name, CASESENSITIVE => 0 );
# custom roles are named uniquely, but just in case there are
diff --git a/lib/RT/Asset.pm b/lib/RT/Asset.pm
index e313db5d63..c29e58f67d 100644
--- a/lib/RT/Asset.pm
+++ b/lib/RT/Asset.pm
@@ -101,6 +101,48 @@ for my $role ('Owner', 'HeldBy', 'Contact') {
);
}
+RT::CustomRole->RegisterLookupType(
+ CustomFieldLookupType() => {
+ FriendlyName => 'Assets',
+ CreateGroupPredicate => sub {
+ my ($object, $role) = @_;
+ if ($object->isa('RT::Catalog')) {
+ # In case catalog level custom role groups got deleted
+ # somehow. Allow to re-create them like default ones.
+ return $role->IsAdded($object->id);
+ }
+ elsif ($object->isa('RT::Asset')) {
+ # see if the role has been applied to the asset's catalog
+ # need to walk around ACLs
+ return $role->IsAdded($object->__Value('Catalog'));
+ }
+
+ return 0;
+ },
+ AppliesToObjectPredicate => sub {
+ my ($object, $role) = @_;
+ return 0 unless $object->CurrentUserHasRight('ShowCatalog');
+
+ # custom roles apply to catalogs, so canonicalize an asset
+ # into its catalog
+ if ($object->isa('RT::Asset')) {
+ $object = $object->CatalogObj;
+ }
+
+ if ($object->isa('RT::Catalog')) {
+ return $role->IsAdded($object->Id);
+ }
+
+ return 0;
+ },
+ Subgroup => {
+ Domain => 'RT::Asset-Role',
+ Table => 'Assets',
+ Parent => 'Catalog',
+ },
+ }
+);
+
=head1 DESCRIPTION
An Asset is a small record object upon which zero to many custom fields are
@@ -262,7 +304,7 @@ sub Create {
}
my $roles = {};
- my @errors = $self->_ResolveRoles( $roles, %args );
+ my @errors = $catalog->_ResolveRoles( $roles, %args );
return (0, @errors) if @errors;
RT->DatabaseHandle->BeginTransaction();
diff --git a/lib/RT/Assets.pm b/lib/RT/Assets.pm
index 046a834e9e..ca7c355be1 100644
--- a/lib/RT/Assets.pm
+++ b/lib/RT/Assets.pm
@@ -89,6 +89,7 @@ our %FIELD_METADATA = (
HeldByGroup => [ 'MEMBERSHIPFIELD' => 'HeldBy', ], #loc_left_pair
Contact => [ 'WATCHERFIELD' => 'Contact', ], #loc_left_pair
ContactGroup => [ 'MEMBERSHIPFIELD' => 'Contact', ], #loc_left_pair
+ CustomRole => [ 'WATCHERFIELD' ], # loc_left_pair
CustomFieldValue => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
CustomField => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
@@ -1217,6 +1218,47 @@ sub _StringLimit {
);
}
+=head2 _CustomRoleDecipher
+
+Try and turn a custom role descriptor (e.g. C<CustomRole.{Engineer}>) into
+(role, column, original name).
+
+=cut
+
+sub _CustomRoleDecipher {
+ my ( $self, $string ) = @_;
+
+ # $column could be core fields like "EmailAddress" or CFs like
+ # "CustomField.{Department}", the CF format is used in OrderByCols.
+ my ( $field, $column ) = ( $string =~ /^\{(.+?)\}(?:\.(.+))?$/ );
+
+ my $role;
+
+ if ( $field =~ /\D/ ) {
+ my $roles = RT::CustomRoles->new( $self->CurrentUser );
+ $roles->LimitToLookupType( RT::Asset->CustomFieldLookupType );
+ $roles->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
+
+ # in case there are multiple matches, bail out as we
+ # don't know which one to use
+ $role = $roles->First;
+ if ($role) {
+ if ( $roles->Next ) {
+ RT->Logger->error(
+ "Ambiguous custom role named '$field' in AssetSQL; skipping. Perhaps specify __CustomRole.{id}__ instead."
+ );
+ $role = undef;
+ }
+ }
+ }
+ else {
+ $role = RT::CustomRole->new( $self->CurrentUser );
+ $role->Load($field);
+ }
+
+ return ( $role, $column, $field );
+}
+
=head2 _WatcherLimit
Handle watcher limits. (Requestor, CC, etc..)
@@ -1238,18 +1280,25 @@ sub _WatcherLimit {
my $meta = $FIELD_METADATA{ $field };
my $type = $meta->[1] || '';
my $class = $meta->[2] || 'Asset';
+ my $column = $rest{SUBKEY};
+
+ if ($field eq 'CustomRole') {
+ my ($role, $col, $original_name) = $self->_CustomRoleDecipher( $column );
+ $column = $col || 'id';
+ $type = $role ? $role->GroupType : $original_name;
+ }
# Bail if the subfield is not allowed
- if ( $rest{SUBKEY}
- and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
+ if ( $column
+ and not grep { $_ eq $column } @{$SEARCHABLE_SUBFIELDS{'User'}})
{
- die "Invalid watcher subfield: '$rest{SUBKEY}'";
+ die "Invalid watcher subfield: '$column'";
}
$self->RoleLimit(
TYPE => $type,
CLASS => "RT::$class",
- FIELD => $rest{SUBKEY},
+ FIELD => $column,
OPERATOR => $op,
VALUE => $value,
SUBCLAUSE => "assetsql",
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 3f78fc216b..ac7b6d89e4 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -57,7 +57,8 @@ use Scalar::Util 'blessed';
use base 'RT::Record';
use Role::Basic 'with';
-with "RT::Record::Role::Rights";
+with "RT::Record::Role::Rights",
+ "RT::Record::Role::LookupType";
sub Table {'CustomFields'}
@@ -218,7 +219,6 @@ our %FieldTypes = (
my %BUILTIN_GROUPINGS;
-my %FRIENDLY_LOOKUP_TYPES = ();
__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc
__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
@@ -1414,120 +1414,6 @@ sub SetLookupType {
return $self->_Set(Field => 'LookupType', Value =>$lookup);
}
-=head2 LookupTypes
-
-Returns an array of LookupTypes available
-
-=cut
-
-
-sub LookupTypes {
- my $self = shift;
- return sort keys %FRIENDLY_LOOKUP_TYPES;
-}
-
-=head2 FriendlyLookupType
-
-Returns a localized description of the type of this custom field
-
-=cut
-
-sub FriendlyLookupType {
- my $self = shift;
- my $lookup = shift || $self->LookupType;
-
- return ($self->loc( $FRIENDLY_LOOKUP_TYPES{$lookup} ))
- if defined $FRIENDLY_LOOKUP_TYPES{$lookup};
-
- my @types = map { s/^RT::// ? $self->loc($_) : $_ }
- grep { defined and length }
- split( /-/, $lookup )
- or return;
-
- state $LocStrings = [
- "[_1] objects", # loc
- "[_1]'s [_2] objects", # loc
- "[_1]'s [_2]'s [_3] objects", # loc
- ];
- return ( $self->loc( $LocStrings->[$#types], @types ) );
-}
-
-=head1 RecordClassFromLookupType
-
-Returns the type of Object referred to by ObjectCustomFields' ObjectId column
-
-Optionally takes a LookupType to use instead of using the value on the loaded
-record. In this case, the method may be called on the class instead of an
-object.
-
-=cut
-
-sub RecordClassFromLookupType {
- my $self = shift;
- my $type = shift || $self->LookupType;
- my ($class) = ($type =~ /^([^-]+)/);
- unless ( $class ) {
- if (blessed($self) and $self->LookupType eq $type) {
- $RT::Logger->error(
- "Custom Field #". $self->id
- ." has incorrect LookupType '$type'"
- );
- } else {
- RT->Logger->error("Invalid LookupType passed as argument: $type");
- }
- return undef;
- }
- return $class;
-}
-
-=head1 ObjectTypeFromLookupType
-
-Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
-
-Optionally takes a LookupType to use instead of using the value on the loaded
-record. In this case, the method may be called on the class instead of an
-object.
-
-=cut
-
-sub ObjectTypeFromLookupType {
- my $self = shift;
- my $type = shift || $self->LookupType;
- my ($class) = ($type =~ /([^-]+)$/);
- unless ( $class ) {
- if (blessed($self) and $self->LookupType eq $type) {
- $RT::Logger->error(
- "Custom Field #". $self->id
- ." has incorrect LookupType '$type'"
- );
- } else {
- RT->Logger->error("Invalid LookupType passed as argument: $type");
- }
- return undef;
- }
- return $class;
-}
-
-sub CollectionClassFromLookupType {
- my $self = shift;
- my $record_class = shift || $self->RecordClassFromLookupType;
-
- return undef unless $record_class;
-
- my $collection_class;
- if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
- $collection_class = $record_class.'Collection';
- } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
- $collection_class = $record_class.'es';
- } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
- $collection_class = $record_class.'s';
- } else {
- $RT::Logger->error("Can not find a collection class for record class '$record_class'");
- return undef;
- }
- return $collection_class;
-}
-
=head2 Groupings Object|Class Name, Queue Name|Catalog Name
Returns a (sorted and lowercased) list of the groupings in which this custom
@@ -1640,20 +1526,6 @@ sub RegisterBuiltInGroupings {
$BUILTIN_GROUPINGS{''} = { map { %$_ } values %BUILTIN_GROUPINGS };
}
-=head1 IsOnlyGlobal
-
-Certain custom fields (users, groups) should only be added globally;
-codify that set here for reference.
-
-=cut
-
-sub IsOnlyGlobal {
- my $self = shift;
-
- return ($self->LookupType =~ /^RT::(?:Group|User)/io);
-
-}
-
=head1 AddedTo
Returns collection with objects this custom field is added to.
@@ -2141,31 +2013,6 @@ sub CurrentUserCanSee {
return 0;
}
-=head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME
-
-Tell RT that a certain object accepts custom fields via a lookup type and
-provide a friendly name for such CFs.
-
-Examples:
-
- 'RT::Queue-RT::Ticket' => "Tickets", # loc
- 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
- 'RT::User' => "Users", # loc
- 'RT::Group' => "Groups", # loc
- 'RT::Queue' => "Queues", # loc
-
-This is a class method.
-
-=cut
-
-sub RegisterLookupType {
- my $self = shift;
- my $path = shift;
- my $friendly_name = shift;
-
- $FRIENDLY_LOOKUP_TYPES{$path} = $friendly_name;
-}
-
=head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
Gets or sets the C<IncludeContentForValue> for this custom field. RT
diff --git a/lib/RT/CustomRole.pm b/lib/RT/CustomRole.pm
index fa4ec0f747..cf22198491 100644
--- a/lib/RT/CustomRole.pm
+++ b/lib/RT/CustomRole.pm
@@ -55,6 +55,9 @@ use base 'RT::Record';
use RT::CustomRoles;
use RT::ObjectCustomRole;
+use Role::Basic 'with';
+with "RT::Record::Role::LookupType";
+
=head1 NAME
RT::CustomRole - user-defined role groups
@@ -79,6 +82,7 @@ Create takes a hash of values and creates a row in the database:
varchar(255) 'Description'.
int(11) 'MaxValues'.
varchar(255) 'EntryHint'.
+ varchar(255) 'LookupType'.
smallint(6) 'Disabled'.
=cut
@@ -90,6 +94,7 @@ sub Create {
Description => '',
MaxValues => 0,
EntryHint => '',
+ LookupType => '',
Disabled => 0,
@_,
);
@@ -99,13 +104,16 @@ sub Create {
}
{
- my ($val, $msg) = $self->_ValidateName( $args{'Name'} );
+ my ($val, $msg) = $self->_ValidateName( $args{'Name'}, $args{'LookupType'} );
return ($val, $msg) unless $val;
}
$args{'Disabled'} ||= 0;
$args{'MaxValues'} = int $args{'MaxValues'};
+ # backwards compatibility; used to be the only possibility
+ $args{'LookupType'} ||= 'RT::Queue-RT::Ticket';
+
$RT::Handle->BeginTransaction;
my ($ok, $msg) = $self->SUPER::Create(
@@ -113,6 +121,7 @@ sub Create {
Description => $args{'Description'},
MaxValues => $args{'MaxValues'},
EntryHint => $args{'EntryHint'},
+ LookupType => $args{'LookupType'},
Disabled => $args{'Disabled'},
);
unless ($ok) {
@@ -152,9 +161,9 @@ sub _RegisterAsRole {
my $self = shift;
my $id = $self->Id;
- RT::Ticket->RegisterRole(
+ $self->ObjectTypeFromLookupType->RegisterRole(
Name => $self->GroupType,
- EquivClasses => ['RT::Queue'],
+ EquivClasses => [$self->RecordClassFromLookupType],
Single => $self->SingleValue,
UserDefined => 1,
@@ -171,17 +180,10 @@ sub _RegisterAsRole {
my $role = RT::CustomRole->new(RT->SystemUser);
$role->Load($id);
- if ($object->isa('RT::Queue')) {
- # In case queue level custom role groups got deleted
- # somehow. Allow to re-create them like default ones.
- return $role->IsAdded($object->id);
- }
- elsif ($object->isa('RT::Ticket')) {
- # see if the role has been applied to the ticket's queue
- # need to walk around ACLs because of the common case of
- # (e.g. Everyone) having the CreateTicket right but not
- # ShowTicket
- return $role->IsAdded($object->__Value('Queue'));
+ if ( $role->Id ) {
+ if (my $predicate = $role->LookupTypeRegistration($role->LookupType, 'CreateGroupPredicate')) {
+ return $predicate->($object, $role);
+ }
}
return 0;
@@ -205,16 +207,10 @@ sub _RegisterAsRole {
my $role = RT::CustomRole->new(RT->SystemUser);
$role->Load($id);
- if ( $object->isa('RT::Ticket') || $object->isa('RT::Queue') ) {
- return 0 unless $object->CurrentUserHasRight('SeeQueue');
-
- # custom roles apply to queues, so canonicalize a ticket
- # into its queue
- if ( $object->isa('RT::Ticket') ) {
- $object = $object->QueueObj;
+ if ( $role->Id ) {
+ if (my $predicate = $role->LookupTypeRegistration($role->LookupType, 'AppliesToObjectPredicate')) {
+ return $predicate->($object, $role);
}
-
- return $role->IsAdded( $object->Id );
}
return 0;
@@ -235,7 +231,7 @@ sub _RegisterAsRole {
sub _UnregisterAsRole {
my $self = shift;
- RT::Ticket->UnregisterRole($self->GroupType);
+ $self->ObjectTypeFromLookupType->UnregisterRole($self->GroupType);
}
=head2 Load ID/NAME
@@ -265,8 +261,9 @@ a new custom role. Returns undef if there's already a role by that name.
sub ValidateName {
my $self = shift;
my $name = shift;
+ my $type = shift || $self->LookupType || 'RT::Queue-RT::Ticket';
- my ($ok, $msg) = $self->_ValidateName($name);
+ my ($ok, $msg) = $self->_ValidateName($name, $type);
return $ok ? 1 : 0;
}
@@ -274,6 +271,7 @@ sub ValidateName {
sub _ValidateName {
my $self = shift;
my $name = shift;
+ my $type = shift || $self->LookupType || 'RT::Queue-RT::Ticket';
return (undef, "Role name is required") unless length $name;
@@ -282,17 +280,34 @@ sub _ValidateName {
return ($ok, $self->loc("'[_1]' is not a valid name.", $name));
}
- # These roles are builtin, so avoid any potential confusion
- if ($name =~ m{^( cc
+ if ( $type eq 'RT::Queue-RT::Ticket' ) {
+ # These roles are ticket builtin, so avoid any potential confusion
+ if (
+ $name =~ m{^( cc
| admin[ ]?cc
| requestors?
| owner
- ) $}xi) {
- return (undef, $self->loc("Role already exists") );
+ ) $}xi
+ )
+ {
+ return ( undef, $self->loc("Role already exists") );
+ }
+ }
+ else {
+ # These roles are asset builtin, so avoid any potential confusion
+ if (
+ $name =~ m{^( heldby
+ | contacts?
+ | owner
+ ) $}xi
+ )
+ {
+ return ( undef, $self->loc("Role already exists") );
+ }
}
my $temp = RT::CustomRole->new(RT->SystemUser);
- $temp->LoadByCols(Name => $name);
+ $temp->LoadByCols(Name => $name, LookupType => $type);
if ( $temp->Name && $temp->id != ($self->id||0)) {
return (undef, $self->loc("Role already exists") );
@@ -301,6 +316,23 @@ sub _ValidateName {
return (1);
}
+=head2 ValidateLookupType TYPE
+
+Takes a custom role lookup type. Returns true unless there's another role
+with the same name and lookup type.
+
+=cut
+
+sub ValidateLookupType {
+ my $self = shift;
+ my $type = shift;
+ if ( $self->Id && lc $self->LookupType ne lc $type ) {
+ return $self->ValidateName( $self->Name, $type );
+ }
+ return 1;
+}
+
+
=head2 Delete
Delete this object. You should Disable instead.
@@ -375,7 +407,7 @@ sub NotAddedTo {
=head2 AddToObject
-Adds (applies) this custom role to the provided queue (ObjectId).
+Adds (applies) this custom role to the provided object (ObjectId).
Accepts a param hash of:
@@ -383,7 +415,7 @@ Accepts a param hash of:
=item C<ObjectId>
-Queue name or id.
+Object id of the class corresponding with L</LookupType>.
=item C<SortOrder>
@@ -400,26 +432,30 @@ sub AddToObject {
my $self = shift;
my %args = @_%2? (ObjectId => @_) : (@_);
- my $queue = RT::Queue->new( $self->CurrentUser );
- $queue->Load( $args{'ObjectId'} );
- return (0, $self->loc('Invalid queue'))
- unless $queue->id;
+ my $class = $self->RecordClassFromLookupType;
+ my $object = $class->new( $self->CurrentUser );
+ $object->Load( $args{'ObjectId'} );
+ unless ($object->id) {
+ RT->Logger->warn("Unable to load $class '$args{'ObjectId'}' for custom role " . $self->Id);
+ return (0, $self->loc('Unable to load [_1]', $args{'ObjectId'}))
+ }
- $args{'ObjectId'} = $queue->id;
+ $args{'ObjectId'} = $object->id;
return ( 0, $self->loc('Permission Denied') )
- unless $queue->CurrentUserHasRight('AdminCustomRoles');
+ unless $object->CurrentUserHasRight('AdminCustomRoles');
+
my $rec = RT::ObjectCustomRole->new( $self->CurrentUser );
my ( $status, $add ) = $rec->Add( %args, CustomRole => $self );
my $msg;
- $msg = $self->loc("[_1] added to queue [_2]", $self->Name, $queue->Name) if $status;
+ $msg = $self->loc("[_1] added to queue [_2]", $self->Name, $object->Name) if $status;
return ( $add, $msg );
}
=head2 RemoveFromObject
-Removes this custom role from the provided queue (ObjectId).
+Removes this custom role from the provided object (ObjectId).
Accepts a param hash of:
@@ -427,7 +463,7 @@ Accepts a param hash of:
=item C<ObjectId>
-Queue name or id.
+Object id of the class corresponding with L</LookupType>.
=back
@@ -440,19 +476,25 @@ sub RemoveFromObject {
my $self = shift;
my %args = @_%2? (ObjectId => @_) : (@_);
- my $queue = RT::Queue->new( $self->CurrentUser );
- $queue->Load( $args{'ObjectId'} );
- return (0, $self->loc('Invalid queue id'))
- unless $queue->id;
+ my $class = $self->RecordClassFromLookupType;
+ my $object = $class->new( $self->CurrentUser );
+ $object->Load( $args{'ObjectId'} );
+ unless ($object->id) {
+ RT->Logger->warn("Unable to load $class '$args{'ObjectId'}' for custom role " . $self->Id);
+ return (0, $self->loc('Unable to load [_1]', $args{'ObjectId'}))
+ }
+
+ $args{'ObjectId'} = $object->id;
return ( 0, $self->loc('Permission Denied') )
- unless $queue->CurrentUserHasRight('AdminCustomRoles');
+ unless $object->CurrentUserHasRight('AdminCustomRoles');
+
my $rec = RT::ObjectCustomRole->new( $self->CurrentUser );
$rec->LoadByCols( CustomRole => $self->id, ObjectId => $args{'ObjectId'} );
return (0, $self->loc('Custom role is not added') ) unless $rec->id;
my ( $status, $delete ) = $rec->Delete;
my $msg;
- $msg = $self->loc("[_1] removed from queue [_2]", $self->Name, $queue->Name) if $status;
+ $msg = $self->loc("[_1] removed from queue [_2]", $self->Name, $object->Name) if $status;
return ( $delete, $msg );
}
@@ -561,6 +603,39 @@ sub SetMaxValues {
return ($ok, $msg);
}
+=head2 LookupType
+
+Returns the current value of LookupType.
+(In the database, LookupType is stored as varchar(255).)
+
+=head2 SetLookupType VALUE
+
+
+Set LookupType to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, LookupType will be stored as a varchar(255).)
+
+=cut
+
+sub SetLookupType {
+ my $self = shift;
+ my $lookup = shift;
+ if ( $lookup ne $self->LookupType ) {
+ # Okay... We need to invalidate our existing relationships
+ RT::ObjectCustomRole->new($self->CurrentUser)->DeleteAll( CustomRole => $self );
+ }
+
+ $self->_UnregisterAsRole;
+
+ my ($ok, $msg) = $self->_Set(Field => 'LookupType', Value => $lookup);
+
+ # update EquivClasses declaration
+ $self->_RegisterAsRole;
+ RT->System->CustomRoleCacheNeedsUpdate(1);
+
+ return ($ok, $msg);
+}
+
=head2 EntryHint
Returns the current value of EntryHint.
@@ -615,62 +690,65 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
=cut
-sub _SetGroupsDisabledForQueue {
+sub _SetGroupsDisabledForObject {
my $self = shift;
my $value = shift;
- my $queue = shift;
+ my $object = shift;
- # set disabled on the queue group
- my $queue_group = RT::Group->new($self->CurrentUser);
- $queue_group->LoadRoleGroup(
+ # set disabled on the object group
+ my $object_group = RT::Group->new($self->CurrentUser);
+ $object_group->LoadRoleGroup(
Name => $self->GroupType,
- Object => $queue,
+ Object => $object,
);
- if (!$queue_group->Id) {
+ if (!$object_group->Id) {
$RT::Handle->Rollback;
- $RT::Logger->error("Couldn't find role group for " . $self->GroupType . " on queue " . $queue->Id);
+ $RT::Logger->error("Couldn't find role group for " . $self->GroupType . " on " . ref($object) . " #" . $object->Id);
return(undef);
}
- my ($ok, $msg) = $queue_group->SetDisabled($value);
+ my ($ok, $msg) = $object_group->SetDisabled($value);
unless ($ok) {
$RT::Handle->Rollback;
$RT::Logger->error("Couldn't SetDisabled($value) on role group: $msg");
return(undef);
}
- # disable each existant ticket group
- my $ticket_groups = RT::Groups->new($self->CurrentUser);
-
- if ($value) {
- $ticket_groups->LimitToEnabled;
- }
- else {
- $ticket_groups->LimitToDeleted;
- }
-
- $ticket_groups->Limit(FIELD => 'Domain', OPERATOR => 'LIKE', VALUE => "RT::Ticket-Role", CASESENSITIVE => 0 );
- $ticket_groups->Limit(FIELD => 'Name', OPERATOR => '=', VALUE => $self->GroupType, CASESENSITIVE => 0);
+ my $subgroup_config = $self->LookupTypeRegistration($self->LookupType, 'Subgroup');
+ if ($subgroup_config) {
+ # disable each existant ticket group
+ my $groups = RT::Groups->new($self->CurrentUser);
- my $tickets = $ticket_groups->Join(
- ALIAS1 => 'main',
- FIELD1 => 'Instance',
- TABLE2 => 'Tickets',
- FIELD2 => 'Id',
- );
- $ticket_groups->Limit(
- ALIAS => $tickets,
- FIELD => 'Queue',
- VALUE => $queue->Id,
- );
+ if ($value) {
+ $groups->LimitToEnabled;
+ }
+ else {
+ $groups->LimitToDeleted;
+ }
- while (my $ticket_group = $ticket_groups->Next) {
- my ($ok, $msg) = $ticket_group->SetDisabled($value);
- unless ($ok) {
- $RT::Handle->Rollback;
- $RT::Logger->error("Couldn't SetDisabled($value) ticket role group: $msg");
- return(undef);
+ $groups->Limit(FIELD => 'Domain', OPERATOR => 'LIKE', VALUE => $subgroup_config->{Domain}, CASESENSITIVE => 0 );
+ $groups->Limit(FIELD => 'Name', OPERATOR => '=', VALUE => $self->GroupType, CASESENSITIVE => 0);
+
+ my $objects = $groups->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'Instance',
+ TABLE2 => $subgroup_config->{Table},
+ FIELD2 => 'Id',
+ );
+ $groups->Limit(
+ ALIAS => $objects,
+ FIELD => $subgroup_config->{Parent},
+ VALUE => $object->Id,
+ );
+
+ while (my $group = $groups->Next) {
+ my ($ok, $msg) = $group->SetDisabled($value);
+ unless ($ok) {
+ $RT::Handle->Rollback;
+ $RT::Logger->error("Couldn't SetDisabled($value) role group: $msg");
+ return(undef);
+ }
}
}
}
@@ -753,6 +831,8 @@ sub _CoreAccessible {
{read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
EntryHint =>
{read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
+ LookupType =>
+ {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
Creator =>
{read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
Created =>
diff --git a/lib/RT/CustomRoles.pm b/lib/RT/CustomRoles.pm
index f49a9f9e4d..f9f01db2fa 100644
--- a/lib/RT/CustomRoles.pm
+++ b/lib/RT/CustomRoles.pm
@@ -98,6 +98,11 @@ subsystem, suitable for system startup.
sub RegisterRoles {
my $class = shift;
+ for my $type ( keys %RT::Record::Role::Roles::ROLES ) {
+ %{ $RT::Record::Role::Roles::ROLES{$type} } = map { $_ => $RT::Record::Role::Roles::ROLES{$type}{$_} }
+ grep { !/^RT::CustomRole-/ } keys %{$RT::Record::Role::Roles::ROLES{$type}};
+ }
+
my $roles = $class->new(RT->SystemUser);
$roles->UnLimit;
@@ -156,6 +161,19 @@ sub LimitToMultipleValue {
);
}
+=head2 LimitToLookupType
+
+Takes LookupType and limits collection.
+
+=cut
+
+sub LimitToLookupType {
+ my $self = shift;
+ my $lookup = shift;
+
+ $self->Limit( FIELD => 'LookupType', VALUE => "$lookup" );
+}
+
=head2 ApplySortOrder
Sort custom roles according to the order provided by the object custom roles.
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 7f9854a204..9fc202d001 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -1074,7 +1074,7 @@ sub _AddMember {
}
return (1, $self->loc("[_1] set to [_2]",
- $self->loc($self->Name), $new_member_obj->Object->Name) )
+ $self->Label, $new_member_obj->Object->Name) )
if $self->SingleMemberRoleGroup;
return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 6e827ffe28..51c6d2fca8 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4360,11 +4360,16 @@ sub ProcessAssetRoleMembers {
elsif ($arg =~ /^SetRoleMember-(.+)$/) {
my $role = $1;
my $group = $object->RoleGroup($role);
+ if ( !$group->id ) {
+ $group = $object->_CreateRoleGroup($role);
+ }
next unless $group->id and $group->SingleMemberRoleGroup;
- next if $ARGS{$arg} eq $group->UserMembersObj->First->Name;
+ my $original_user = $group->UserMembersObj->First || RT->Nobody;
+ $ARGS{$arg} ||= 'Nobody';
+ next if $ARGS{$arg} eq $original_user->Name;
my ($ok, $msg) = $object->AddRoleMember(
Type => $role,
- User => $ARGS{$arg} || 'Nobody',
+ User => $ARGS{$arg},
);
push @results, $msg;
}
diff --git a/lib/RT/ObjectCustomRole.pm b/lib/RT/ObjectCustomRole.pm
index c66161044f..554004134b 100644
--- a/lib/RT/ObjectCustomRole.pm
+++ b/lib/RT/ObjectCustomRole.pm
@@ -57,11 +57,11 @@ use RT::ObjectCustomRoles;
=head1 NAME
-RT::ObjectCustomRole - record representing addition of a custom role to a queue
+RT::ObjectCustomRole - record representing addition of a custom role to an object
=head1 DESCRIPTION
-This record is created if you want to add a custom role to a queue.
+This record is created if you want to add a custom role to an object.
Inherits methods from L<RT::Record::AddAndSort>.
@@ -79,12 +79,16 @@ sub Table {'ObjectCustomRoles'}
=head2 ObjectCollectionClass
-Returns class name of collection of records custom roles can be added to.
-Now it's only L<RT::Queue>, so 'RT::Queues' is returned.
+Returns class name of collection of records this custom role can be added to
+by consulting the custom role's C<LookupType>.
=cut
-sub ObjectCollectionClass {'RT::Queues'}
+sub ObjectCollectionClass {
+ my $self = shift;
+ my %args = (@_);
+ return $args{'CustomRole'}->CollectionClassFromLookupType;
+}
=head2 CustomRoleObj
@@ -100,22 +104,30 @@ sub CustomRoleObj {
return $obj;
}
-=head2 QueueObj
+=head2 Object
-Returns the L<RT::Queue> object which this ObjectCustomRole is added to
+Returns the object which this ObjectCustomRole is added to
=cut
+sub Object {
+ my $self = shift;
+ my $role = $self->CustomRoleObj;
+ my $class = $role->RecordClassFromLookupType;
+ my $object = $class->new($self->CurrentUser);
+ $object->Load($self->ObjectId);
+ return $object;
+}
+
sub QueueObj {
my $self = shift;
- my $queue = RT::Queue->new($self->CurrentUser);
- $queue->Load($self->ObjectId);
- return $queue;
+ RT->Deprecated( Instead => "Object", Remove => '5.2' );
+ return $self->Object(@_);
}
=head2 Add
-Adds the custom role to the queue and creates (or re-enables) that queue's role
+Adds the custom role to the object and creates (or re-enables) that object's role
group.
=cut
@@ -132,15 +144,15 @@ sub Add {
return(undef);
}
- my $queue = $self->QueueObj;
+ my $object = $self->Object;
my $role = $self->CustomRoleObj;
# see if we already have this role group (which can happen if you
- # add a role to a queue, remove it, then add it back in)
+ # add a role to an object, remove it, then add it back in)
my $existing = RT::Group->new($self->CurrentUser);
$existing->LoadRoleGroup(
Name => $role->GroupType,
- Object => $queue,
+ Object => $object,
);
if ($existing->Id) {
@@ -150,7 +162,7 @@ sub Add {
my $group = RT::Group->new($self->CurrentUser);
my ($ok, $msg) = $group->CreateRoleGroup(
Name => $role->GroupType,
- Object => $queue,
+ Object => $object,
);
unless ($ok) {
@@ -168,7 +180,7 @@ sub Add {
=head2 Delete
-Removes the custom role from the queue and disables that queue's role group.
+Removes the custom role from the object and disables that object's role group.
=cut
@@ -194,7 +206,7 @@ sub FindDependencies {
$self->SUPER::FindDependencies($walker, $deps);
$deps->Add( out => $self->CustomRoleObj );
- $deps->Add( out => $self->QueueObj );
+ $deps->Add( out => $self->Object );
}
sub Serialize {
diff --git a/lib/RT/ObjectCustomRoles.pm b/lib/RT/ObjectCustomRoles.pm
index 9baabb514b..64352fc94b 100644
--- a/lib/RT/ObjectCustomRoles.pm
+++ b/lib/RT/ObjectCustomRoles.pm
@@ -106,6 +106,25 @@ sub LimitToObjectId {
);
}
+sub LimitToLookupType {
+ my $self = shift;
+ my $lookup = shift;
+
+ $self->{'_crs_alias'} ||= $self->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'CustomRole',
+ TABLE2 => 'CustomRoles',
+ FIELD2 => 'id',
+ );
+ $self->Limit(
+ ALIAS => $self->{'_crs_alias'},
+ FIELD => 'LookupType',
+ OPERATOR => '=',
+ VALUE => $lookup,
+ );
+}
+
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Principal.pm b/lib/RT/Principal.pm
index 27200c3c0f..48ab66e13f 100644
--- a/lib/RT/Principal.pm
+++ b/lib/RT/Principal.pm
@@ -447,7 +447,7 @@ sub HasRights {
if ( $custom_role->id && !$custom_role->Disabled ) {
my $added;
for my $object ( @{ $args{'EquivObjects'} } ) {
- next unless $object->isa('RT::Queue');
+ next unless $object->isa('RT::Queue') || $object->isa('RT::Catalog');
if ( $custom_role->IsAdded( $object->id ) ) {
$added = 1;
last;
@@ -699,7 +699,7 @@ sub RolesWithRight {
if ( $custom_role->id && !$custom_role->Disabled ) {
my $added;
for my $object ( @{ $args{'EquivObjects'} } ) {
- next unless $object->isa('RT::Queue');
+ next unless $object->isa('RT::Queue') || $object->isa('RT::Catalog');
if ( $custom_role->IsAdded( $object->id ) ) {
$added = 1;
last;
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 8aba033035..1f3af9c9d0 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -481,6 +481,7 @@ sub CustomRoles {
my $roles = RT::CustomRoles->new( $self->CurrentUser );
if ( $self->CurrentUserHasRight('SeeQueue') ) {
$roles->LimitToObjectId( $self->Id );
+ $roles->LimitToLookupType(RT::Ticket->CustomFieldLookupType);
$roles->ApplySortOrder;
}
else {
@@ -1086,6 +1087,7 @@ sub FindDependencies {
# Object Custom Roles
$objs = RT::ObjectCustomRoles->new( $self->CurrentUser );
$objs->LimitToObjectId($self->Id);
+ $objs->LimitToLookupType(RT::Ticket->CustomFieldLookupType);
$deps->Add( in => $objs );
}
diff --git a/lib/RT/Record/Role/LookupType.pm b/lib/RT/Record/Role/LookupType.pm
new file mode 100644
index 0000000000..4f9fa2ba1a
--- /dev/null
+++ b/lib/RT/Record/Role/LookupType.pm
@@ -0,0 +1,290 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+# <sales@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 }}}
+
+package RT::Record::Role::LookupType;
+
+use strict;
+use warnings;
+use 5.010;
+
+use Role::Basic;
+use Scalar::Util qw(blessed);
+
+=head1 NAME
+
+RT::Record::Role::LookupType - Common methods for records which have a LookupType
+
+=head1 DESCRIPTION
+
+Certain records, like custom fields, can be applied to different types of
+records (tickets, transactions, groups, users, etc). This role implements
+such I<LookupType> concerns.
+
+This role does not manage concerns relating to specifying which records
+of a class (as in L<RT::ObjectCustomField>).
+
+=head1 REQUIRES
+
+=head2 L<RT::Record::Role>
+
+=head2 LookupType
+
+A C<LookupType> method which returns this record's lookup type is required.
+Currently unenforced at compile-time due to poor interactions with
+L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if
+this method isn't available in consuming classes, however.
+
+=cut
+
+with 'RT::Record::Role';
+
+=head1 PROVIDES
+
+=head2 RegisterLookupType LOOKUPTYPE OPTIONS
+
+Tell RT that a certain object accepts records of this role via a lookup
+type. I<OPTIONS> is a hash reference for which the following keys are
+used:
+
+=over 4
+
+=item FriendlyName
+
+The string to display in the UI to users for this lookup type
+
+=back
+
+For backwards compatibility, I<OPTIONS> may also be a string which is
+interpreted as specifying the I<FriendlyName>.
+
+Examples:
+
+ 'RT::Queue-RT::Ticket' => "Tickets", # loc
+ 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
+ 'RT::User' => "Users", # loc
+ 'RT::Group' => "Groups", # loc
+ 'RT::Queue' => "Queues", # loc
+
+This is a class method.
+
+=cut
+
+my %REGISTRY = ();
+
+sub RegisterLookupType {
+ my $class = shift;
+ my $path = shift;
+ my $options = shift;
+
+ die "RegisterLookupType is a class method" if blessed($class);
+
+ $options = {
+ FriendlyName => $options,
+ } if !ref($options);
+
+ $REGISTRY{$class}{$path} = $options;
+}
+
+=head2 LookupTypes
+
+Returns an array of LookupTypes available for this record or class
+
+=cut
+
+sub LookupTypes {
+ my $self = shift;
+ my $class = blessed($self) || $self;
+ return sort keys %{ $REGISTRY{ $class } };
+}
+
+=head2 LookupTypeRegistration [PATH] [OPTION]
+
+Returns the arguments of calls to L</RegisterLookupType>. With no arguments, returns a hash of hashes,
+where the first-level key is the path (corresponding with L<RT::Record/CustomFieldLookupType>) and
+the second-level hash is the option names. If path and option are provided, it looks up in that
+nested hash structure to provide the desired information.
+
+=cut
+
+sub LookupTypeRegistration {
+ my $self = shift;
+ my $class = blessed($self) || $self;
+
+ my $path = shift
+ or return %{ $REGISTRY{$class}};
+
+ my $option = shift
+ or return %{ $REGISTRY{$class}{$path}};
+
+ my $ret = $REGISTRY{$class}{$path}{$option};
+ return $ret;
+}
+
+=head2 FriendlyLookupType
+
+Returns a localized description of the LookupType of this record
+
+=cut
+
+sub FriendlyLookupType {
+ my $self = shift;
+ my $lookup = shift || $self->LookupType;
+
+ my $class = blessed($self) || $self;
+
+ if (my $friendly = $self->LookupTypeRegistration($lookup, 'FriendlyName')) {
+ return $self->loc($friendly);
+ }
+
+ my @types = map { s/^RT::// ? $self->loc($_) : $_ }
+ grep { defined and length }
+ split( /-/, $lookup )
+ or return;
+
+ state $LocStrings = [
+ "[_1] objects", # loc
+ "[_1]'s [_2] objects", # loc
+ "[_1]'s [_2]'s [_3] objects", # loc
+ ];
+ return ( $self->loc( $LocStrings->[$#types], @types ) );
+}
+
+=head1 RecordClassFromLookupType
+
+Returns the type of Object referred to by ObjectCustomFields' ObjectId column.
+(The first part of the LookupType, e.g. the C<RT::Queue> of
+C<RT::Queue-RT::Ticket-RT::Transaction>)
+
+Optionally takes a LookupType to use instead of using the value on the loaded
+record. In this case, the method may be called on the class instead of an
+object.
+
+=cut
+
+sub RecordClassFromLookupType {
+ my $self = shift;
+ my $type = shift || $self->LookupType;
+ my ($class) = ($type =~ /^([^-]+)/);
+ unless ( $class ) {
+ if (blessed($self) and $self->LookupType eq $type) {
+ $RT::Logger->error(
+ blessed($self) . " #". $self->id
+ ." has incorrect LookupType '$type'"
+ );
+ } else {
+ RT->Logger->error("Invalid LookupType passed as argument: $type");
+ }
+ return undef;
+ }
+ return $class;
+}
+
+=head1 ObjectTypeFromLookupType
+
+Returns the ObjectType for this record. (The last part of the LookupType,
+e.g. the C<RT::Transaction> of C<RT::Queue-RT::Ticket-RT::Transaction>)
+
+Optionally takes a LookupType to use instead of using the value on the loaded
+record. In this case, the method may be called on the class instead of an
+object.
+
+=cut
+
+sub ObjectTypeFromLookupType {
+ my $self = shift;
+ my $type = shift || $self->LookupType;
+ my ($class) = ($type =~ /([^-]+)$/);
+ unless ( $class ) {
+ if (blessed($self) and $self->LookupType eq $type) {
+ $RT::Logger->error(
+ blessed($self) . " #". $self->id
+ ." has incorrect LookupType '$type'"
+ );
+ } else {
+ RT->Logger->error("Invalid LookupType passed as argument: $type");
+ }
+ return undef;
+ }
+ return $class;
+}
+
+sub CollectionClassFromLookupType {
+ my $self = shift;
+
+ my $record_class = shift || $self->RecordClassFromLookupType;
+ return undef unless $record_class;
+
+ my $collection_class;
+ if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
+ $collection_class = $record_class.'Collection';
+ } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
+ $collection_class = $record_class.'es';
+ } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
+ $collection_class = $record_class.'s';
+ } else {
+ $RT::Logger->error("Can not find a collection class for record class '$record_class'");
+ return undef;
+ }
+ return $collection_class;
+}
+
+=head1 IsOnlyGlobal
+
+Certain record types (users, groups) should only be added globally;
+codify that set here for reference.
+
+=cut
+
+sub IsOnlyGlobal {
+ my $self = shift;
+
+ return ($self->LookupType =~ /^RT::(?:Group|User)/io);
+
+}
+
+1;
diff --git a/lib/RT/Record/Role/Roles.pm b/lib/RT/Record/Role/Roles.pm
index ffcfa5240d..44466123d2 100644
--- a/lib/RT/Record/Role/Roles.pm
+++ b/lib/RT/Record/Role/Roles.pm
@@ -308,16 +308,16 @@ sub Roles {
map { [ $_, $self->_ROLES->{$_} ] }
keys %{ $self->_ROLES };
- # Cache at ticket/queue object level mainly to reduce calls of
- # custom role's AppliesToObjectPredicate for performance.
- if ( ref($self) =~ /RT::(?:Ticket|Queue)/ ) {
+ # Cache at object level mainly to reduce calls of custom role's
+ # AppliesToObjectPredicate for performance.
+ if ( ref($self) =~ /RT::(?:Ticket|Queue|Asset|Catalog)/ ) {
$self->{_Roles}{$key} = \@roles;
}
return @roles;
}
{
- my %ROLES;
+ our %ROLES;
sub _ROLES {
my $class = ref($_[0]) || $_[0];
return $ROLES{$class} ||= {};
@@ -851,4 +851,48 @@ you see faster create times.
=cut
+=head2 CustomRoleObj
+
+Returns the L<RT::CustomRole> object for this role if and only if it's
+backed by a custom role. If it's a core role (e.g. Ticket Requestors),
+returns C<undef>.
+
+=cut
+
+sub CustomRoleObj {
+ my $self = shift;
+ my $name = shift;
+
+ if (my ($id) = $name =~ /^RT::CustomRole-(\d+)$/) {
+ my $role = RT::CustomRole->new($self->CurrentUser);
+ my ( $ret, $msg ) = $role->Load($id);
+ if ( $ret ) {
+ return $role;
+ }
+ else {
+ RT->Logger->warning("Couldn't load custom role #$id: $msg");
+ }
+ }
+
+ return undef;
+}
+
+
+=head2 RoleAddresses
+
+Takes a role name and returns a string of all the email addresses for
+users in that role.
+
+=cut
+
+sub RoleAddresses {
+ my $self = shift;
+ my $role = shift;
+
+ if ( $self->CurrentUserCanSee ) {
+ return $self->RoleGroup($role)->MemberEmailAddressesAsString;
+ }
+ return undef;
+}
+
1;
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index d63881f79e..5592929bb3 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -114,6 +114,50 @@ for my $role (sort keys %ROLES) {
);
}
+RT::CustomRole->RegisterLookupType(
+ CustomFieldLookupType() => {
+ FriendlyName => 'Tickets',
+ CreateGroupPredicate => sub {
+ my ($object, $role) = @_;
+ if ($object->isa('RT::Queue')) {
+ # In case queue level custom role groups got deleted
+ # somehow. Allow to re-create them like default ones.
+ return $role->IsAdded($object->id);
+ }
+ elsif ($object->isa('RT::Ticket')) {
+ # see if the role has been applied to the ticket's queue
+ # need to walk around ACLs because of the common case of
+ # (e.g. Everyone) having the CreateTicket right but not
+ # ShowTicket
+ return $role->IsAdded($object->__Value('Queue'));
+ }
+
+ return 0;
+ },
+ AppliesToObjectPredicate => sub {
+ my ($object, $role) = @_;
+ return 0 unless $object->CurrentUserHasRight('SeeQueue');
+
+ # custom roles apply to queues, so canonicalize a ticket
+ # into its queue
+ if ($object->isa('RT::Ticket')) {
+ $object = $object->QueueObj;
+ }
+
+ if ($object->isa('RT::Queue')) {
+ return $role->IsAdded($object->Id);
+ }
+
+ return 0;
+ },
+ Subgroup => {
+ Domain => 'RT::Ticket-Role',
+ Table => 'Tickets',
+ Parent => 'Queue',
+ },
+ }
+);
+
our %MERGE_CACHE = (
effective => {},
merged => {},
@@ -830,25 +874,6 @@ sub CcAddresses {
return $self->RoleAddresses('Cc');
}
-=head2 RoleAddresses
-
-Takes a role name and returns a string of all the email addresses for
-users in that role
-
-=cut
-
-sub RoleAddresses {
- my $self = shift;
- my $role = shift;
-
- unless ( $self->CurrentUserHasRight('ShowTicket') ) {
- return undef;
- }
- return ( $self->RoleGroup($role)->MemberEmailAddressesAsString);
-}
-
-
-
=head2 Requestor
Takes nothing.
@@ -3002,7 +3027,7 @@ sub CurrentUserCanSee {
my ($what, $txn) = @_;
return 0 unless $self->CurrentUserHasRight('ShowTicket');
- return 1 if $what ne "Transaction";
+ return 1 if ( $what // '' ) ne "Transaction";
# If it's a comment, we need to be extra special careful
my $type = $txn->__Value('Type');
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 1278f3b000..183932117f 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1112,6 +1112,7 @@ sub _CustomRoleDecipher {
if ( $field =~ /\D/ ) {
my $roles = RT::CustomRoles->new( $self->CurrentUser );
+ $roles->LimitToLookupType(RT::Ticket->CustomFieldLookupType);
$roles->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
# custom roles are named uniquely, but just in case there are
diff --git a/sbin/rt-validator.in b/sbin/rt-validator.in
index fa6730547b..ba67577e23 100644
--- a/sbin/rt-validator.in
+++ b/sbin/rt-validator.in
@@ -420,7 +420,7 @@ push @CHECKS, 'User Defined Group Name uniqueness' => sub {
push @CHECKS, 'Custom Role Name uniqueness' => sub {
return check_uniqueness(
'CustomRoles',
- columns => ['Name'],
+ columns => ['Name', 'LookupType'],
action => sub {
return unless prompt(
'Rename', "Found a custom role with a non-unique Name."
diff --git a/share/html/Admin/CustomFields/Modify.html b/share/html/Admin/CustomFields/Modify.html
index 2cfc5be13e..daef955a53 100644
--- a/share/html/Admin/CustomFields/Modify.html
+++ b/share/html/Admin/CustomFields/Modify.html
@@ -96,8 +96,10 @@
% }
<&| /Elements/LabeledValue, Label => loc("Applies to") &>
- <& /Admin/Elements/SelectCustomFieldLookupType,
+ <& /Admin/Elements/SelectLookupType,
+ Class => 'RT::CustomField',
Name => "LookupType",
+ Object => $CustomFieldObj,
Default => $CustomFieldObj->LookupType || $LookupType,
&>
</&>
diff --git a/share/html/Admin/CustomRoles/Modify.html b/share/html/Admin/CustomRoles/Modify.html
index 115a64588d..35b14b7fb9 100644
--- a/share/html/Admin/CustomRoles/Modify.html
+++ b/share/html/Admin/CustomRoles/Modify.html
@@ -64,6 +64,14 @@
<input class="form-control" type="text" name="Description" value="<% $Create ? "" : $RoleObj->Description || $Description || '' %>" size="60" />
</&>
+<&| /Elements/LabeledValue, Label => loc('Applies To') &>
+ <& /Admin/Elements/SelectLookupType,
+ Name => "LookupType",
+ Object => $RoleObj,
+ Default => $RoleObj->LookupType || $LookupType,
+ &>
+</&>
+
<&| /Elements/LabeledValue, Label => loc("Entry Hint") &>
<input class="form-control" type="text" name="EntryHint" value="<% $Create ? "" : $RoleObj->EntryHint || $EntryHint || '' %>" size="60" />
</&>
@@ -125,7 +133,7 @@ $EnabledChecked = 'checked="checked"';
unless ($Create) {
if ( defined $id && $id eq 'new' ) {
- my ($val, $msg) = $RoleObj->Create( Name => $Name );
+ my ($val, $msg) = $RoleObj->Create( Name => $Name, LookupType => $LookupType );
if (!$val) {
$Create = 1; # Create failed, so bring us back to step 1
push @results, $msg;
@@ -140,7 +148,7 @@ unless ($Create) {
if ( $RoleObj->Id ) {
$title = loc('Configuration for role [_1]', $RoleObj->Name );
- my @attribs = qw(Description Name EntryHint Disabled);
+ my @attribs = qw(Description Name EntryHint LookupType Disabled);
# we just created the role
if (!$id || $id eq 'new') {
@@ -198,4 +206,5 @@ $SetEnabled => undef
$SetMultiple => undef
$Multiple => undef
$Enabled => undef
+$LookupType => RT::Ticket->CustomFieldLookupType
</%ARGS>
diff --git a/share/html/Admin/CustomRoles/Objects.html b/share/html/Admin/CustomRoles/Objects.html
index c8bc2f7a27..d3e954b6d0 100644
--- a/share/html/Admin/CustomRoles/Objects.html
+++ b/share/html/Admin/CustomRoles/Objects.html
@@ -56,8 +56,8 @@
<h2><&|/l&>Selected objects</&></h2>
<& /Elements/CollectionList,
- OrderBy => 'id',
- Order => 'ASC',
+ OrderBy => $class->isa('RT::Queue') ? ['SortOrder', 'Name'] : 'id',
+ Order => $class->isa('RT::Queue') ? ['ASC', 'ASC'] : 'ASC',
%ARGS,
Collection => $added,
Rows => 0,
@@ -74,8 +74,8 @@
<h2><&|/l&>Unselected objects</&></h2>
<& /Elements/CollectionList,
- OrderBy => 'Name',
- Order => 'ASC',
+ OrderBy => $class->isa('RT::Queue') ? ['SortOrder', 'Name'] : 'id',
+ Order => $class->isa('RT::Queue') ? ['ASC', 'ASC'] : 'ASC',
%ARGS,
Collection => $not_added,
Rows => $rows,
@@ -102,6 +102,8 @@ my $role = RT::CustomRole->new( $session{'CurrentUser'} );
$role->Load($id) or Abort(loc("Could not load custom role #[_1]", $id));
$id = $role->id;
+my $class = $role->RecordClassFromLookupType;
+
if ($role->Disabled) {
Abort(loc("Cannot modify objects of disabled custom role #[_1]", $id));
}
@@ -132,8 +134,12 @@ if ( $Update ) {
my $added = $role->AddedTo;
my $not_added = $role->NotAddedTo;
-my $format = RT->Config->Get('AdminSearchResultFormat')->{'Queues'};
-my $rows = RT->Config->Get('AdminSearchResultRows')->{'Queues'} || 50;
+my $collection_class = ref($added);
+$collection_class =~ s/^RT:://;
+
+my $format = RT->Config->Get('AdminSearchResultFormat')->{$collection_class}
+ || '__id__,__Name__';
+my $rows = RT->Config->Get('AdminSearchResultRows')->{$collection_class} || 50;
my $title = loc('Modify associated objects for [_1]', $role->Name);
diff --git a/share/html/Admin/CustomRoles/index.html b/share/html/Admin/CustomRoles/index.html
index c366cfbab7..f308d6b4af 100644
--- a/share/html/Admin/CustomRoles/index.html
+++ b/share/html/Admin/CustomRoles/index.html
@@ -92,11 +92,12 @@
<em><&|/l&>No custom roles matching search criteria found.</&></em>
% } else {
<& /Elements/CollectionList,
- OrderBy => 'Name',
- Order => 'ASC',
+ OrderBy => 'LookupType|Name',
+ Order => 'ASC|ASC',
Rows => $Rows,
%ARGS,
Format => $Format,
+ DisplayFormat => ($Type? '' : '__FriendlyLookupType__,'). $Format,
Collection => $roles,
AllowSorting => 1,
PassArguments => [qw(
@@ -110,6 +111,7 @@ my $title = loc("Select a Custom Role");
my $roles = RT::CustomRoles->new($session{'CurrentUser'});
$roles->FindAllRows if $FindDisabled;
+$roles->LimitToLookupType( $Type ) if $Type;
if ( defined $SearchString && length $SearchString ) {
$roles->Limit(
@@ -128,6 +130,7 @@ my $Rows = RT->Config->Get('AdminSearchResultRows')->{'CustomRoles'} || 50;
</%INIT>
<%ARGS>
+$Type => ''
$FindDisabled => 0
$Format => undef
diff --git a/share/html/Admin/Elements/SelectCustomFieldLookupType b/share/html/Admin/Elements/SelectCustomFieldLookupType
index f43d543829..95d7970b83 100644
--- a/share/html/Admin/Elements/SelectCustomFieldLookupType
+++ b/share/html/Admin/Elements/SelectCustomFieldLookupType
@@ -45,16 +45,11 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<select class="form-control selectpicker" name="<%$Name%>">
-%for my $option ($cf->LookupTypes) {
-<option value="<%$option%>"<%defined ($Default) && ($option eq $Default) && qq[ selected="selected"] |n%>><% $cf->FriendlyLookupType($option) %></option>
-%}
-</select>
-<%INIT>
-my $cf = RT::CustomField->new($session{'CurrentUser'});
+<& SelectLookupType, %ARGS, Class => 'RT::CustomField' &>
+<%INIT>
+RT->Deprecated(
+ Remove => '5.4',
+ Instead => 'SelectLookupType',
+);
</%INIT>
-<%ARGS>
-$Default=> ''
-$Name => 'LookupType'
-</%ARGS>
diff --git a/share/html/Admin/Elements/SelectLookupType b/share/html/Admin/Elements/SelectLookupType
new file mode 100644
index 0000000000..7b470555cd
--- /dev/null
+++ b/share/html/Admin/Elements/SelectLookupType
@@ -0,0 +1,61 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2022 Best Practical Solutions, LLC
+%# <sales@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 }}}
+<select class="form-control selectpicker" name="<%$Name%>">
+%for my $option ($Object->LookupTypes) {
+<option value="<%$option%>"<%defined ($Default) && ($option eq $Default) && qq[ selected="selected"] |n%>><% $Object->FriendlyLookupType($option) %></option>
+%}
+</select>
+<%INIT>
+$Object ||= $Class->new($session{'CurrentUser'});
+</%INIT>
+<%ARGS>
+$Default => ''
+$Name => 'LookupType'
+$Object => undef
+$Class => ''
+</%ARGS>
diff --git a/share/html/Asset/Create.html b/share/html/Asset/Create.html
index f5e91da737..c71526cc61 100644
--- a/share/html/Asset/Create.html
+++ b/share/html/Asset/Create.html
@@ -65,7 +65,7 @@
<div class="col-6">
<&| /Widgets/TitleBox, title => loc("People"), class => "asset-people", title_class => "inverse" &>
- <& Elements/EditPeople, %ARGS, AssetObj => $asset &>
+ <& Elements/EditPeople, %ARGS, AssetObj => $asset, CatalogObj => $catalog &>
</&>
</div>
</div>
@@ -156,7 +156,7 @@ if ($id eq "new") {
ProcessLinksForCreate( ARGSRef => \%ARGS ),
map {
$_ => $ARGS{$_}
- } $asset->Roles,
+ } $catalog->Roles,
);
# Handle basic fields
diff --git a/share/html/Asset/Elements/AssetSearchPeople b/share/html/Asset/Elements/AssetSearchPeople
index fece788d36..32022d0b1a 100644
--- a/share/html/Asset/Elements/AssetSearchPeople
+++ b/share/html/Asset/Elements/AssetSearchPeople
@@ -46,10 +46,10 @@
%#
%# END BPS TAGGED BLOCK }}}
<&| /Widgets/TitleBox, class => "asset-search-people", title => loc('People') &>
-% for my $role (RT::Asset->Roles) {
+% for my $role ($CatalogObj->Roles) {
<div class="asset-role-<% CSSClass($role) %> form-row">
<div class="label col-2">
- <% loc($role) %>
+ <% RT::Asset->LabelForRole($role) %>
</div>
<div class="value col-4">
<input class="form-control" type="text" id="Role.<% $role %>" name="Role.<% $role %>"
diff --git a/share/html/Asset/Elements/EditCatalogPeople b/share/html/Asset/Elements/EditCatalogPeople
index 08c7e00bc7..d52663c439 100644
--- a/share/html/Asset/Elements/EditCatalogPeople
+++ b/share/html/Asset/Elements/EditCatalogPeople
@@ -52,8 +52,8 @@ $Object
</%init>
% for my $role ($Object->Roles( ACLOnly => 0 )) {
<div class="role-<% CSSClass($role) %> role">
- <h3><% loc($role) %></h3>
- <& EditRoleMembers, Group => $Object->RoleGroup($role) &>
+ <h3><% $Object->LabelForRole($role) %></h3>
+ <& EditRoleMembers, Object => $Object, Role => $role &>
</div>
% }
diff --git a/share/html/Asset/Elements/EditPeople b/share/html/Asset/Elements/EditPeople
index 8db9a7f1f9..79120caa6b 100644
--- a/share/html/Asset/Elements/EditPeople
+++ b/share/html/Asset/Elements/EditPeople
@@ -45,12 +45,19 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-% for my $role ( $AssetObj->Roles ) {
- <&| /Elements/LabeledValue, Label => loc($role), Class => "asset-people-".CSSClass($role) &>
- <& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1, ($AssetObj->Role($role)->{Single} ? () : (AutocompleteType => 'Principals', AutocompleteMultiple => 1)) &>
+% for my $role ( $object->Roles ) {
+% my $custom_role = $object->CustomRoleObj($role);
+% my $hint = $custom_role ? $custom_role->EntryHint : '';
+ <&| /Elements/LabeledValue, Label => $object->LabelForRole($role), Class => "asset-people-".CSSClass($role), LabelTooltip => $hint &>
+ <& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1, ($object->Role($role)->{Single} ? () : (AutocompleteType => 'Principals', AutocompleteMultiple => 1)) &>
</&>
% }
+<%init>
+my $object = $AssetObj->Id ? $AssetObj : $CatalogObj;
+</%init>
+
<%args>
$AssetObj
+$CatalogObj
</%args>
diff --git a/share/html/Asset/Elements/EditRoleMembers b/share/html/Asset/Elements/EditRoleMembers
index a99b66e322..e0b109529b 100644
--- a/share/html/Asset/Elements/EditRoleMembers
+++ b/share/html/Asset/Elements/EditRoleMembers
@@ -46,17 +46,20 @@
%#
%# END BPS TAGGED BLOCK }}}
<%args>
-$Group => undef
+$Object
+$Role
$Recursively => 0
</%args>
<%init>
+my $Group = $Object->RoleGroup($Role);
my $field_name = "RemoveRoleMember-" . $Group->Name;
</%init>
<ul class="role-members list-group list-group-compact">
% my $Users = $Group->UserMembersObj( Recursively => $Recursively );
-% if ($Group->SingleMemberRoleGroup) {
+% if ($Object->Role($Role)->{Single}) {
+% my $user = $Users->First || RT->Nobody;
<li class="list-group-item">
- <input class="form-control selectpicker" type="text" value="<% $Users->First->Name %>" name="SetRoleMember-<% $Group->Name %>" id="SetRoleMember-<% $Group->Name %>" data-autocomplete="Users" data-autocomplete-return="Name" />
+ <input class="form-control selectpicker" type="text" value="<% $user->Name %>" name="SetRoleMember-<% $Group->Name %>" id="SetRoleMember-<% $Group->Name %>" data-autocomplete="Users" data-autocomplete-return="Name" />
</li>
% } else {
% while ( my $user = $Users->Next ) {
diff --git a/share/html/Asset/Elements/SelectRoleType b/share/html/Asset/Elements/SelectRoleType
index 17f296d813..953232f483 100644
--- a/share/html/Asset/Elements/SelectRoleType
+++ b/share/html/Asset/Elements/SelectRoleType
@@ -55,6 +55,6 @@ $AllowNull => 0
<option value=""></option>
% }
% for my $role ($Object->Roles( ACLOnly => 0, Single => 0 )) {
- <option value="<% $role %>"><% loc($role) %></option>
+ <option value="<% $role %>"><% $Object->LabelForRole($role) %></option>
% }
</select>
diff --git a/share/html/Asset/Elements/ShowPeople b/share/html/Asset/Elements/ShowPeople
index 6b35fdf2e6..fb505decae 100644
--- a/share/html/Asset/Elements/ShowPeople
+++ b/share/html/Asset/Elements/ShowPeople
@@ -54,14 +54,14 @@ my $CatalogObj = $AssetObj->CatalogObj;
% for my $role ($AssetObj->Roles) {
<div class="form-row">
<div class="label col-3">
- <% loc($role) %>:
+ <% $AssetObj->LabelForRole($role) %>:
</div>
<div class="value col-9">
<div class="user-accordion accordion">
% if ($AssetObj->Role($role)->{Single}) {
% my $users = $AssetObj->RoleGroup($role)->UserMembersObj(Recursively => 0);
% $users->FindAllRows;
-% my $user = $users->Next;
+% my $user = $users->Next || RT->Nobody;
% if ( $user->id != RT->Nobody->id ) {
<& ShowRoleMembers, Group => $AssetObj->RoleGroup($role), Role => $role &>
% } else {
diff --git a/share/html/Asset/Elements/ShowRoleMembers b/share/html/Asset/Elements/ShowRoleMembers
index e050b9117e..ab134f2d1a 100644
--- a/share/html/Asset/Elements/ShowRoleMembers
+++ b/share/html/Asset/Elements/ShowRoleMembers
@@ -51,7 +51,7 @@
% next if $user->id == RT->Nobody->id;
<div class="accordion-item">
- <span class="accordion-title collapsed toggle" data-toggle="collapse" data-target="#<% $Role %>-user-<% $user->id %>" aria-expanded="false" aria-controls="<% $Role %>-user-<% $user->id %>" id="<% $Role %>-user-<% $user->id %>-title" >
+ <span class="accordion-title collapsed toggle" data-toggle="collapse" data-target="[id='<% $Role %>-user-<% $user->id %>']" aria-expanded="false" aria-controls="<% $Role %>-user-<% $user->id %>" id="<% $Role %>-user-<% $user->id %>-title" >
% if ($Title) {
<& /Elements/ShowUser, User => $user, Link => 1 &>
diff --git a/share/html/Asset/Search/Bulk.html b/share/html/Asset/Search/Bulk.html
index f1331b3af2..43b716296d 100644
--- a/share/html/Asset/Search/Bulk.html
+++ b/share/html/Asset/Search/Bulk.html
@@ -124,29 +124,29 @@
</&>
<&| /Widgets/TitleBox, title => loc("People"), class => "asset-people asset-bulk-people", title_class => "inverse" &>
-% for my $rname ( $asset->Roles( ACLOnly => 0 ) ) {
-% my $role = $asset->Role( $rname );
-% if ( $role->{'Single'} ) {
+% for my $rname ( $asset->Roles( ACLOnly => 0, Single => 1 ), map { $_->GroupType } @{ $single_roles->ItemsArrayRef } ) {
% my $input = "SetRoleMember-$rname";
<div class="form-row">
<div class="col-6">
- <&| /Elements/LabeledValue, Label => loc($rname) &>
+ <&| /Elements/LabeledValue, Label => RT::Asset->LabelForRole($rname) &>
<input class="form-control" type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" />
</&>
</div>
</div>
-% } else {
+% }
+
+% for my $rname ( $asset->Roles( ACLOnly => 0, Single => 0 ), map { $_->GroupType } @{ $multi_roles->ItemsArrayRef } ) {
% my $input = "AddRoleMember-$rname";
<div class="form-row">
<div class="col-6">
- <&| /Elements/LabeledValue, Label => loc("Add [_1]", loc($rname)) &>
+ <&| /Elements/LabeledValue, Label => loc("Add [_1]", RT::Asset->LabelForRole($rname)) &>
<input class="form-control" type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" />
</&>
</div>
% $input = "RemoveRoleMember-$rname";
<div class="col-6">
- <&| /Elements/LabeledValue, Label => loc("Remove [_1]", loc($rname)) &>
+ <&| /Elements/LabeledValue, Label => loc("Remove [_1]", RT::Asset->LabelForRole($rname)) &>
<input class="form-control" type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" />
<div class="custom-control custom-checkbox">
@@ -157,7 +157,6 @@
</div>
</div>
% }
-% }
% my $people_cfs = $cfs->Clone;
% $people_cfs->LimitToGrouping( 'RT::Asset' => 'People');
% if ( $people_cfs->Count ) {
@@ -229,6 +228,9 @@ delete $ARGS{$_} foreach grep { $ARGS{$_} =~ /^$/ } keys %ARGS;
$DECODED_ARGS->{'UpdateAssetAll'} = 1 unless @UpdateAsset;
my $cfs;
+my $single_roles = RT::CustomRoles->new( $session{CurrentUser} );
+my $multi_roles = RT::CustomRoles->new( $session{CurrentUser} );
+
if ( $ARGS{Query} ) {
$cfs = RT::CustomFields->new( $session{'CurrentUser'} );
$cfs->LimitToLookupType( RT::Asset->CustomFieldLookupType );
@@ -252,9 +254,26 @@ if ( $ARGS{Query} ) {
}
}
$cfs->LimitToGlobalOrObjectId(@ids);
+
+ if ( @ids ) {
+ $single_roles->LimitToObjectId($_) for @ids;
+ $multi_roles->LimitToObjectId($_) for @ids;
+ }
}
else {
$cfs = $catalog_obj->AssetCustomFields;
+ $single_roles->LimitToObjectId( $catalog_obj->Id );
+ $multi_roles->LimitToObjectId( $catalog_obj->Id );
+}
+
+if ( $single_roles->_isLimited ) {
+ $single_roles->LimitToLookupType( RT::Asset->CustomFieldLookupType );
+ $single_roles->LimitToSingleValue;
+}
+
+if ( $multi_roles->_isLimited ) {
+ $multi_roles->LimitToLookupType( RT::Asset->CustomFieldLookupType );
+ $multi_roles->LimitToMultipleValue;
}
if ( $ARGS{'CreateLinkedTicket'} ){
diff --git a/share/html/Elements/ColumnMap b/share/html/Elements/ColumnMap
index 8a5b89fcb1..da1f866efe 100644
--- a/share/html/Elements/ColumnMap
+++ b/share/html/Elements/ColumnMap
@@ -294,7 +294,12 @@ $WCOLUMN_MAP = $COLUMN_MAP = {
my $role_obj = $m->notes($key);
if (!$role_obj) {
$role_obj = RT::CustomRole->new($_[0]->CurrentUser);
- $role_obj->Load($role_name);
+ if ($role_name =~ /^\d+$/) {
+ $role_obj->Load($role_name);
+ }
+ else {
+ $role_obj->LoadByCols(Name => $role_name, LookupType => $_[0]->CustomFieldLookupType);
+ }
RT->Logger->notice("Unable to load custom role $role_name")
unless $role_obj->Id;
diff --git a/share/html/Elements/RT__CustomRole/ColumnMap b/share/html/Elements/RT__CustomRole/ColumnMap
index 758b24bd89..2c2f7ba6e2 100644
--- a/share/html/Elements/RT__CustomRole/ColumnMap
+++ b/share/html/Elements/RT__CustomRole/ColumnMap
@@ -63,7 +63,16 @@ my $COLUMN_MAP = {
title => $c, attribute => $c,
value => sub { return $_[0]->$c() },
} }
- qw(Name Description EntryHint)
+ qw(Name Description LookupType EntryHint)
+ ),
+
+ map(
+ { my $c = $_; my $short = $c; $short =~ s/^Friendly//;
+ $c => {
+ title => $short, attribute => $short,
+ value => sub { return $_[0]->$c() },
+ } }
+ qw(FriendlyLookupType FriendlyType FriendlyPattern)
),
MaxValues => {
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index 1b9a2855f0..d038d587f5 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -154,6 +154,7 @@
</&>
% my $single_roles = RT::CustomRoles->new($session{CurrentUser});
+% $single_roles->LimitToLookupType(RT::Ticket->CustomFieldLookupType);
% $single_roles->LimitToSingleValue;
% $single_roles->LimitToObjectId($_) for keys %$seen_queues;
% while (my $role = $single_roles->Next) {
@@ -163,6 +164,7 @@
% }
% my $multi_roles = RT::CustomRoles->new($session{CurrentUser});
+% $multi_roles->LimitToLookupType(RT::Ticket->CustomFieldLookupType);
% $multi_roles->LimitToMultipleValue;
% $multi_roles->LimitToObjectId($_) for keys %$seen_queues;
% while (my $role = $multi_roles->Next) {
diff --git a/share/html/Search/Elements/BuildFormatString b/share/html/Search/Elements/BuildFormatString
index 15f0351c18..12e630d857 100644
--- a/share/html/Search/Elements/BuildFormatString
+++ b/share/html/Search/Elements/BuildFormatString
@@ -144,6 +144,19 @@ elsif ( $Class eq 'RT::Assets' ) {
push @fields, "CustomFieldView.{" . $CustomField->Name . "}";
}
+ my $CustomRoles = RT::CustomRoles->new( $session{'CurrentUser'} );
+ foreach my $id ( keys %catalogs ) {
+
+ # Gotta load up the $catalog object, since catalogs get stored by name now.
+ my $catalog = RT::Catalog->new( $session{'CurrentUser'} );
+ $catalog->Load($id);
+ next unless $catalog->Id;
+ $CustomRoles->LimitToObjectId( $catalog->Id );
+ }
+ $CustomRoles->LimitToLookupType( RT::Asset->CustomFieldLookupType ) if $CustomRoles->_isLimited;
+ while ( my $role = $CustomRoles->Next ) {
+ push @fields, 'CustomRole.{' . $role->Name . '}';
+ }
}
else {
$Format ||= RT->Config->Get('DefaultSearchResultFormat');
@@ -211,6 +224,7 @@ else {
next unless $queue->Id;
$CustomRoles->LimitToObjectId( $queue->Id );
}
+ $CustomRoles->LimitToLookupType(RT::Ticket->CustomFieldLookupType) if $CustomRoles->_isLimited;
my @user_fields = qw/id Name EmailAddress Organization RealName City Country/;
my $user_cfs = RT::CustomFields->new( $session{CurrentUser} );
diff --git a/share/html/Search/Elements/PickBasics b/share/html/Search/Elements/PickBasics
index 4e134b1e81..22ff9d67a0 100644
--- a/share/html/Search/Elements/PickBasics
+++ b/share/html/Search/Elements/PickBasics
@@ -275,7 +275,7 @@ elsif ( $Class eq 'RT::Assets' ) {
Field => {
Type => 'component',
Path => 'SelectPersonType',
- Arguments => { Default => 'Owner', Class => 'RT::Assets' },
+ Arguments => { Default => 'Owner', Class => 'RT::Assets', Catalogs => \%catalogs },
},
Op => {
Type => 'component',
diff --git a/share/html/Search/Elements/PickCustomRoles b/share/html/Search/Elements/PickCustomRoles
index b1acbad2f3..ab86e39f04 100644
--- a/share/html/Search/Elements/PickCustomRoles
+++ b/share/html/Search/Elements/PickCustomRoles
@@ -47,18 +47,35 @@
%# END BPS TAGGED BLOCK }}}
<%ARGS>
%queues => ()
+%catalogs => ()
</%ARGS>
<%INIT>
RT->Deprecated( Message => '/Search/Elements/PickCustomRoles is obsolete', Remove => '5.2' );
my $CustomRoles = RT::CustomRoles->new( $session{'CurrentUser'});
-foreach my $id (keys %queues) {
- # Gotta load up the $queue object, since queues get stored by name now.
- my $queue = RT::Queue->new($session{'CurrentUser'});
- $queue->Load($id);
- next unless $queue->Id;
- $CustomRoles->LimitToObjectId($queue->Id);
+if ( %queues ) {
+ foreach my $id (keys %queues) {
+ # Gotta load up the $queue object, since queues get stored by name now.
+ my $queue = RT::Queue->new($session{'CurrentUser'});
+ $queue->Load($id);
+ next unless $queue->Id;
+ $CustomRoles->LimitToObjectId($queue->Id);
+ }
+ # If there are no referenced queues, do not limit LookupType to return 0 custom roles.
+ $CustomRoles->LimitToLookupType( RT::Ticket->CustomFieldLookupType ) if $CustomRoles->_isLimited;
}
+elsif ( %catalogs ) {
+ foreach my $id (keys %catalogs) {
+ # Gotta load up the $catalog object, since catalogs get stored by name now.
+ my $catalog = RT::Catalog->new($session{'CurrentUser'});
+ $catalog->Load($id);
+ next unless $catalog->Id;
+ $CustomRoles->LimitToObjectId($catalog->Id);
+ }
+ # If there are no referenced catalogs, do not limit LookupType to return 0 custom roles.
+ $CustomRoles->LimitToLookupType( RT::Asset->CustomFieldLookupType ) if $CustomRoles->_isLimited;
+}
+
$m->callback(
CallbackName => 'MassageCustomRoles',
CustomRoles => $CustomRoles,
diff --git a/share/html/Search/Elements/SelectPersonType b/share/html/Search/Elements/SelectPersonType
index 57e4c8a209..9e029f78c6 100644
--- a/share/html/Search/Elements/SelectPersonType
+++ b/share/html/Search/Elements/SelectPersonType
@@ -83,10 +83,18 @@
<%INIT>
my ( @types, @subtypes );
+my $CustomRoles = RT::CustomRoles->new( $session{'CurrentUser'});
+
if ( $Class eq 'RT::Assets' ) {
@types = qw(Owner HeldBy Contact);
@subtypes = @{ $RT::Assets::SEARCHABLE_SUBFIELDS{'User'} };
+ foreach my $id (keys %Catalogs) {
+ my $catalog = RT::Catalog->new($session{'CurrentUser'});
+ $catalog->Load($id);
+ next unless $catalog->Id;
+ $CustomRoles->LimitToObjectId($catalog->Id);
+ }
}
else {
if ($Role) {
@@ -106,23 +114,26 @@ else {
else {
@types = qw(Requestor Cc AdminCc Watcher Owner QueueCc QueueAdminCc QueueWatcher);
- my $CustomRoles = RT::CustomRoles->new( $session{'CurrentUser'});
foreach my $id (keys %Queues) {
my $queue = RT::Queue->new($session{'CurrentUser'});
$queue->Load($id);
next unless $queue->Id;
$CustomRoles->LimitToObjectId($queue->Id);
}
- $m->callback(
- CallbackName => 'MassageCustomRoles',
- CustomRoles => $CustomRoles,
- );
- push @types, map { [ "CustomRole.{" . $_->Name . "}", $_->Name ] } @{ $CustomRoles->ItemsArrayRef };
}
@subtypes = @{ $RT::Tickets::SEARCHABLE_SUBFIELDS{'User'} };
}
+# If there are no referenced queues/catalogs, do not limit LookupType to return 0 custom roles.
+$CustomRoles->LimitToLookupType( $Class->RecordClass->CustomFieldLookupType ) if $CustomRoles->_isLimited;
+
+$m->callback(
+ CallbackName => 'MassageCustomRoles',
+ CustomRoles => $CustomRoles,
+);
+push @types, map { [ "CustomRole.{" . $_->Name . "}", $_->Name ] } @{ $CustomRoles->ItemsArrayRef };
+
$m->callback(Types => \@types, Subtypes => \@subtypes);
</%INIT>
@@ -136,4 +147,5 @@ $Name => 'WatcherType'
$Role => undef
@Roles => ()
%Queues => ()
+%Catalogs => ()
</%ARGS>
diff --git a/t/customroles/assets.t b/t/customroles/assets.t
new file mode 100644
index 0000000000..314041f908
--- /dev/null
+++ b/t/customroles/assets.t
@@ -0,0 +1,330 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+
+my $general = create_catalog( Name => 'General' );
+my $inbox = create_catalog( Name => 'Inbox' );
+my $specs = create_catalog( Name => 'Specs' );
+my $development = create_catalog( Name => 'Development' );
+
+my $engineer = RT::CustomRole->new(RT->SystemUser);
+my $sales = RT::CustomRole->new(RT->SystemUser);
+my $unapplied = RT::CustomRole->new(RT->SystemUser);
+
+my $linus = RT::Test->load_or_create_user( EmailAddress => 'linus@example.com' );
+my $blake = RT::Test->load_or_create_user( EmailAddress => 'blake@example.com' );
+my $williamson = RT::Test->load_or_create_user( EmailAddress => 'williamson@example.com' );
+my $moss = RT::Test->load_or_create_user( EmailAddress => 'moss@example.com' );
+my $ricky = RT::Test->load_or_create_user( EmailAddress => 'ricky.roma@example.com' );
+
+my $team = RT::Test->load_or_create_group(
+ 'Team',
+ Members => [$blake, $williamson, $moss, $ricky],
+);
+
+sub txn_messages_like {
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my $a = shift;
+ my $re = shift;
+
+ my $txns = $a->Transactions;
+ $txns->Limit(FIELD => 'Type', VALUE => 'SetWatcher');
+ $txns->Limit(FIELD => 'Type', VALUE => 'AddWatcher');
+ $txns->Limit(FIELD => 'Type', VALUE => 'DelWatcher');
+
+ is($txns->Count, scalar(@$re), 'expected number of transactions');
+
+ while (my $txn = $txns->Next) {
+ like($txn->BriefDescription, (shift(@$re) || qr/(?!)/));
+ }
+}
+
+diag 'setup' if $ENV{'TEST_VERBOSE'};
+{
+ ok( RT::Test->add_rights( { Principal => 'Privileged', Right => [ qw(CreateAsset ShowAsset ModifyAsset ShowCatalog) ] } ));
+
+ my ($ok, $msg) = $engineer->Create(
+ Name => 'Engineer-' . $$,
+ LookupType => RT::Asset->CustomFieldLookupType,
+ MaxValues => 1,
+ );
+ ok($ok, "created Engineer role: $msg");
+
+ ($ok, $msg) = $sales->Create(
+ Name => 'Sales-' . $$,
+ LookupType => RT::Asset->CustomFieldLookupType,
+ MaxValues => 0,
+ );
+ ok($ok, "created Sales role: $msg");
+
+ ($ok, $msg) = $unapplied->Create(
+ Name => 'Unapplied-' . $$,
+ LookupType => RT::Asset->CustomFieldLookupType,
+ MaxValues => 0,
+ );
+ ok($ok, "created Unapplied role: $msg");
+
+ ($ok, $msg) = $sales->AddToObject($inbox->id);
+ ok($ok, "added Sales to Inbox: $msg");
+
+ ($ok, $msg) = $sales->AddToObject($specs->id);
+ ok($ok, "added Sales to Specs: $msg");
+
+ ($ok, $msg) = $engineer->AddToObject($specs->id);
+ ok($ok, "added Engineer to Specs: $msg");
+
+ ($ok, $msg) = $engineer->AddToObject($development->id);
+ ok($ok, "added Engineer to Development: $msg");
+}
+
+diag 'create assets in General (no custom roles)' if $ENV{'TEST_VERBOSE'};
+{
+ my $general1 = create_asset(
+ Catalog => 'General',
+ Name => 'an asset',
+ Owner => $williamson->PrincipalId,
+ Contact => [$blake->EmailAddress],
+ );
+ is($general1->Owner->id, $williamson->id, 'owner is correct');
+ is($general1->RoleAddresses('Contact'), $blake->EmailAddress, 'contacts correct');
+ is($general1->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($general1->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($general1->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $general2 = create_asset(
+ Catalog => 'General',
+ Name => 'another asset',
+ Owner => $linus->PrincipalId,
+ Contact => [$moss->EmailAddress, $williamson->EmailAddress],
+ HeldBy => [$blake->EmailAddress],
+ );
+ is($general2->Owner->id, $linus->id, 'owner is correct');
+ is($general2->RoleAddresses('Contact'), (join ', ', sort $moss->EmailAddress, $williamson->EmailAddress), 'contacts correct');
+ is($general2->RoleAddresses('HeldBy'), $blake->EmailAddress, 'heldby correct');
+ is($general2->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($general2->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $general3 = create_asset(
+ Catalog => 'General',
+ Name => 'oops',
+ Owner => $ricky->PrincipalId,
+ $engineer->GroupType => $linus,
+ $sales->GroupType => [$blake->EmailAddress],
+ );
+ is($general3->Owner->id, $ricky->id, 'owner is correct');
+ is($general3->RoleAddresses('Contact'), '', 'no contacts');
+ is($general3->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($general3->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($general3->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+}
+
+diag 'create assets in Inbox (sales role)' if $ENV{'TEST_VERBOSE'};
+{
+ my $inbox1 = create_asset(
+ Catalog => 'Inbox',
+ Name => 'an asset',
+ Owner => $williamson->PrincipalId,
+ Contact => [$blake->EmailAddress],
+ );
+ is($inbox1->Owner->id, $williamson->id, 'owner is correct');
+ is($inbox1->RoleAddresses('Contact'), $blake->EmailAddress, 'contacts correct');
+ is($inbox1->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($inbox1->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($inbox1->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $inbox2 = create_asset(
+ Catalog => 'Inbox',
+ Name => 'another asset',
+ Owner => $linus->PrincipalId,
+ Contact => [$moss->EmailAddress, $williamson->EmailAddress],
+ HeldBy => [$blake->EmailAddress],
+ );
+ is($inbox2->Owner->id, $linus->id, 'owner is correct');
+ is($inbox2->RoleAddresses('Contact'), (join ', ', sort $moss->EmailAddress, $williamson->EmailAddress), 'contacts correct');
+ is($inbox2->RoleAddresses('HeldBy'), $blake->EmailAddress, 'heldby correct');
+ is($inbox2->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($inbox2->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $inbox3 = create_asset(
+ Catalog => 'Inbox',
+ Name => 'oops',
+ Owner => $ricky->PrincipalId,
+ $engineer->GroupType => $linus,
+ $sales->GroupType => [$blake->EmailAddress],
+ );
+ is($inbox3->Owner->id, $ricky->id, 'owner is correct');
+ is($inbox3->RoleAddresses('Contact'), '', 'no contacts');
+ is($inbox3->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($inbox3->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($inbox3->RoleAddresses($sales->GroupType), $blake->EmailAddress, 'got sales');
+
+ my $inbox4 = create_asset(
+ Catalog => 'Inbox',
+ Name => 'more',
+ Owner => $ricky->PrincipalId,
+ $engineer->GroupType => $linus,
+ $sales->GroupType => [$blake->EmailAddress, $williamson->EmailAddress],
+ );
+ is($inbox4->Owner->id, $ricky->id, 'owner is correct');
+ is($inbox4->RoleAddresses('Contact'), '', 'no contacts');
+ is($inbox4->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($inbox4->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($inbox4->RoleAddresses($sales->GroupType), (join ', ', sort $blake->EmailAddress, $williamson->EmailAddress), 'got sales');
+}
+
+diag 'create assets in Specs (both roles)' if $ENV{'TEST_VERBOSE'};
+{
+ my $specs1 = create_asset(
+ Catalog => 'Specs',
+ Name => 'an asset',
+ Owner => $williamson->PrincipalId,
+ Contact => [$blake->EmailAddress],
+ );
+ is($specs1->Owner->id, $williamson->id, 'owner is correct');
+ is($specs1->RoleAddresses('Contact'), $blake->EmailAddress, 'contacts correct');
+ is($specs1->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($specs1->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($specs1->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $specs2 = create_asset(
+ Catalog => 'Specs',
+ Name => 'another asset',
+ Owner => $linus->PrincipalId,
+ Contact => [$moss->EmailAddress, $williamson->EmailAddress],
+ HeldBy => [$blake->EmailAddress],
+ );
+ is($specs2->Owner->id, $linus->id, 'owner is correct');
+ is($specs2->RoleAddresses('Contact'), (join ', ', sort $moss->EmailAddress, $williamson->EmailAddress), 'contacts correct');
+ is($specs2->RoleAddresses('HeldBy'), $blake->EmailAddress, 'heldby correct');
+ is($specs2->RoleAddresses($engineer->GroupType), '', 'no engineer (role not applied to catalog)');
+ is($specs2->RoleAddresses($sales->GroupType), '', 'no sales (role not applied to catalog)');
+
+ my $specs3 = create_asset(
+ Catalog => 'Specs',
+ Name => 'oops',
+ Owner => $ricky->PrincipalId,
+ $engineer->GroupType => $linus,
+ $sales->GroupType => [$blake->EmailAddress],
+ );
+ is($specs3->Owner->id, $ricky->id, 'owner is correct');
+ is($specs3->RoleAddresses('Contact'), '', 'no contacts');
+ is($specs3->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($specs3->RoleAddresses($engineer->GroupType), $linus->EmailAddress, 'got engineer');
+ is($specs3->RoleAddresses($sales->GroupType), $blake->EmailAddress, 'got sales');
+
+ my $specs4 = create_asset(
+ Catalog => 'Specs',
+ Name => 'more',
+ Owner => $ricky->PrincipalId,
+ $engineer->GroupType => $linus,
+ $sales->GroupType => [$blake->EmailAddress, $williamson->EmailAddress],
+ );
+ is($specs4->Owner->id, $ricky->id, 'owner is correct');
+ is($specs4->RoleAddresses('Contact'), '', 'no contacts');
+ is($specs4->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($specs4->RoleAddresses($engineer->GroupType), $linus->EmailAddress, 'got engineer');
+ is($specs4->RoleAddresses($sales->GroupType), (join ', ', sort $blake->EmailAddress, $williamson->EmailAddress), 'got sales');
+}
+
+diag 'update asset in Specs' if $ENV{'TEST_VERBOSE'};
+{
+ my $a = create_asset(
+ Catalog => 'Specs',
+ Name => 'updates',
+ );
+
+ is($a->Owner->id, RT->Nobody->id, 'owner nobody');
+ is($a->RoleAddresses('Contact'), '', 'no contacts');
+ is($a->RoleAddresses('HeldBy'), '', 'no heldby');
+ is($a->RoleAddresses($engineer->GroupType), '', 'no engineer');
+ is($a->RoleAddresses($sales->GroupType), '', 'no sales');
+ is($a->RoleAddresses($unapplied->GroupType), '', 'no unapplied');
+
+ my ($ok, $msg) = $a->AddRoleMember(Type => 'Owner', Principal => $linus->PrincipalObj);
+ ok($ok, "set owner: $msg");
+ is($a->Owner->id, $linus->id, 'owner linus');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => 'Contact', Principal => $ricky->PrincipalObj);
+ ok($ok, "add contact: $msg");
+ is($a->RoleAddresses('Contact'), $ricky->EmailAddress, 'contact ricky');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => 'HeldBy', Principal => $blake->PrincipalObj);
+ ok($ok, "add heldby: $msg");
+ is($a->RoleAddresses('HeldBy'), $blake->EmailAddress, 'heldby blake');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $sales->GroupType, Principal => $ricky->PrincipalObj);
+ ok($ok, "add sales: $msg");
+ is($a->RoleAddresses($sales->GroupType), $ricky->EmailAddress, 'sales ricky');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $sales->GroupType, Principal => $moss->PrincipalObj);
+ ok($ok, "add sales: $msg");
+ is($a->RoleAddresses($sales->GroupType), (join ', ', sort $ricky->EmailAddress, $moss->EmailAddress), 'sales ricky and moss');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $sales->GroupType, Principal => RT->Nobody->PrincipalObj);
+ ok($ok, "add sales: $msg");
+ is($a->RoleAddresses($sales->GroupType), (join ', ', sort $ricky->EmailAddress, $moss->EmailAddress), 'sales ricky and moss');
+
+ ($ok, $msg) = $a->DeleteRoleMember(Type => $sales->GroupType, PrincipalId => $moss->PrincipalId);
+ ok($ok, "remove sales: $msg");
+ is($a->RoleAddresses($sales->GroupType), $ricky->EmailAddress, 'sales ricky');
+
+ ($ok, $msg) = $a->DeleteRoleMember(Type => $sales->GroupType, PrincipalId => $ricky->PrincipalId);
+ ok($ok, "remove sales: $msg");
+ is($a->RoleAddresses($sales->GroupType), '', 'sales empty');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $engineer->GroupType, Principal => $linus->PrincipalObj);
+ ok($ok, "add engineer: $msg");
+ is($a->RoleAddresses($engineer->GroupType), $linus->EmailAddress, 'engineer linus');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $engineer->GroupType, Principal => $blake->PrincipalObj);
+ ok($ok, "add engineer: $msg");
+ is($a->RoleAddresses($engineer->GroupType), $blake->EmailAddress, 'engineer blake (single-member role so linus gets displaced)');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $engineer->GroupType, Principal => RT->Nobody->PrincipalObj);
+ ok($ok, "add engineer: $msg");
+ is($a->RoleAddresses($engineer->GroupType), '', 'engineer nobody (single-member role so blake gets displaced)');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $unapplied->GroupType, Principal => $linus->PrincipalObj);
+ ok(!$ok, "did not add unapplied role member: $msg");
+ like($msg, qr/That role is invalid for this object/);
+ is($a->RoleAddresses($unapplied->GroupType), '', 'no unapplied members');
+
+ txn_messages_like($a, [
+ qr/Owner set to linus\@example\.com/,
+ qr/Contact ricky\.roma\@example\.com added/,
+ qr/Held By blake\@example\.com added/,
+ qr/Sales-$$ ricky\.roma\@example\.com added/,
+ qr/Sales-$$ moss\@example\.com added/,
+ qr/Sales-$$ Nobody in particular added/,
+ qr/Sales-$$ moss\@example\.com deleted/,
+ qr/Sales-$$ ricky\.roma\@example\.com deleted/,
+ qr/Engineer-$$ set to linus\@example\.com/,
+ qr/Engineer-$$ set to blake\@example\.com/,
+ qr/Engineer-$$ set to Nobody in particular/,
+ ]);
+}
+
+diag 'groups can be role members' if $ENV{'TEST_VERBOSE'};
+{
+ my $a = create_asset(
+ Catalog => 'Specs',
+ Name => 'groups',
+ );
+
+ my ($ok, $msg) = $a->AddRoleMember(Type => $sales->GroupType, Principal => $team->PrincipalObj);
+ ok($ok, "add team: $msg");
+ is($a->RoleAddresses($sales->GroupType), (join ', ', sort $blake->EmailAddress, $ricky->EmailAddress, $moss->EmailAddress, $williamson->EmailAddress), 'sales is all the team members');
+
+ ($ok, $msg) = $a->AddRoleMember(Type => $engineer->GroupType, Principal => $team->PrincipalObj);
+ ok(!$ok, "could not add team: $msg");
+ like($msg, qr/cannot be a group/);
+ is($a->RoleAddresses($engineer->GroupType), '', 'engineer is still nobody');
+
+ txn_messages_like($a, [
+ qr/Sales-$$ group Team added/,
+ ]);
+}
+
+done_testing;
diff --git a/t/customroles/web-assets.t b/t/customroles/web-assets.t
new file mode 100644
index 0000000000..0d236aaf06
--- /dev/null
+++ b/t/customroles/web-assets.t
@@ -0,0 +1,279 @@
+use strict;
+use warnings;
+use RT::Test::Assets tests => undef;
+my ($baseurl, $m) = RT::Test::Assets->started_ok;
+ok $m->login, "Logged in agent";
+
+
+my $catalog = create_catalog( Name => "Software" );
+ok $catalog->id, "Created Catalog";
+
+my $owner = RT::Test->load_or_create_user(Name => 'owner', EmailAddress => 'owner@example.com');
+my $licensee = RT::Test->load_or_create_user(Name => 'licensee@example.com', EmailAddress => 'licensee@example.com', Password => 'password');
+
+my $role;
+my ($asset, $asset2, $asset3);
+
+diag "Create custom role and apply it to General assets";
+{
+ $m->follow_link_ok({ id => "admin-custom-roles-create" }, "Custom Role create link");
+ $m->submit_form_ok({
+ with_fields => {
+ Name => 'Licensee',
+ Description => 'The person who licensed the software',
+ LookupType => RT::Asset->CustomFieldLookupType,
+ EntryHint => 'Make sure user has real name set',
+ },
+ }, "submitted create form");
+ $m->text_like(qr/Custom role created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+ ok($id, 'Got role id');
+
+ $role = RT::CustomRole->new(RT->SystemUser);
+ $role->Load($id);
+ is $role->id, $id, "id matches";
+ is $role->Name, "Licensee", "Name matches";
+ is $role->Description, "The person who licensed the software", "Description matches";
+ is $role->LookupType, RT::Asset->CustomFieldLookupType, "LookupType matches";
+ is $role->EntryHint, "Make sure user has real name set", "EntryHint matches";
+
+ ok(!$role->IsAdded($catalog->Id), 'not added to catalog yet');
+
+ $m->follow_link_ok({ id => "page-applies-to" }, "Applies to link");
+ $m->submit_form_ok({
+ with_fields => {
+ ("AddRole-" . $id) => $catalog->Id,
+ },
+ button => 'Update',
+ }, "submitted applies to form");
+ $m->text_contains('Licensee added to queue Software', "Found update message");
+
+ # refresh cache
+ RT::CustomRoles->RegisterRoles;
+
+ ok($role->IsAdded($catalog->Id), 'added to catalog now');
+ is_deeply([sort $catalog->Roles], [sort 'Contact', 'HeldBy', 'Owner', $role->GroupType], '->Roles');
+}
+
+diag "Create asset with custom role";
+{
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id, CatalogChanged => 1 } }, "Picked a catalog");
+ $m->text_contains('Licensee', 'custom role name');
+ $m->content_contains('Make sure user has real name set', 'custom role entry hint');
+
+ $m->submit_form_ok({
+ with_fields => {
+ id => 'new',
+ Name => 'Some Software',
+ Owner => 'owner@example.com',
+ $role->GroupType => 'licensee@example.com',
+ },
+ }, "submitted create form");
+ $m->text_like(qr/Asset .* created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+
+ $asset = RT::Asset->new( RT->SystemUser );
+ $asset->Load($id);
+ is $asset->id, $id, "id matches";
+ is $asset->Name, "Some Software", "Name matches";
+ is $asset->Owner->EmailAddress, 'owner@example.com', "Owner matches";
+ is $asset->RoleAddresses($role->GroupType), 'licensee@example.com', "Licensee matches";
+}
+
+diag "Grant permissions on Licensee";
+{
+ $m->follow_link_ok({ id => "admin-assets-catalogs-select" }, "Admin assets");
+ $m->follow_link_ok({ text => 'Software' }, "Picked a catalog");
+ $m->follow_link_ok({ id => 'page-group-rights' }, "Group rights");
+
+ $m->text_contains('Licensee', 'role group name');
+
+ my $acl_id = $catalog->RoleGroup($role->GroupType)->Id;
+
+ $m->form_name('ModifyGroupRights');
+ $m->tick("SetRights-" . $acl_id . '-RT::Catalog-' . $catalog->id, 'ShowAsset');
+ $m->tick("SetRights-" . $acl_id . '-RT::Catalog-' . $catalog->id, 'ShowCatalog');
+ $m->submit;
+ $m->text_contains("Granted right 'ShowAsset' to Licensee");
+ $m->text_contains("Granted right 'ShowCatalog' to Licensee");
+
+ RT::Principal::InvalidateACLCache();
+}
+
+diag "Create asset without custom role";
+{
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id, CatalogChanged => 1 } }, "Picked a catalog");
+ $m->text_contains('Licensee', 'custom role name');
+ $m->content_contains('Make sure user has real name set', 'custom role entry hint');
+
+ $m->submit_form_ok({
+ with_fields => {
+ id => 'new',
+ Name => 'More Software',
+ Owner => 'owner@example.com',
+ },
+ }, "submitted create form");
+ $m->text_like(qr/Asset .* created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+
+ $asset2 = RT::Asset->new( RT->SystemUser );
+ $asset2->Load($id);
+ is $asset2->id, $id, "id matches";
+ is $asset2->Name, "More Software", "Name matches";
+ is $asset2->Owner->EmailAddress, 'owner@example.com', "Owner matches";
+ is $asset2->RoleAddresses($role->GroupType), '', "No Licensee";
+}
+
+diag "Search by custom role";
+{
+ $m->follow_link_ok({ id => "assets-simple_search" }, "Asset simple search link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->Id } }, "Picked a catalog");
+ $m->submit_form_ok({
+ with_fields => {
+ 'Role.' . $role->GroupType => 'licensee@example.com',
+ },
+ button => 'SearchAssets',
+ }, "Search by role");
+
+ $m->text_contains('Some Software', 'search hit');
+ $m->text_lacks('More Software', 'search miss');
+
+ $m->submit_form_ok({
+ with_fields => {
+ 'Role.' . $role->GroupType => '',
+ '!Role.' . $role->GroupType => 'licensee@example.com',
+ },
+ button => 'SearchAssets',
+ }, "Search by role");
+
+ $m->text_lacks('Some Software', 'search miss');
+ $m->text_contains('More Software', 'search hit');
+}
+
+diag "Search by custom role";
+{
+ $m->follow_link_ok({ id => "assets-search" }, "Asset search link");
+ $m->submit_form_ok({ with_fields => { ValueOfCatalog => $catalog->Id }, button => 'AddClause' }, "Picked a catalog");
+
+ my $form = $m->form_name('BuildQuery');
+ my @watcher_options = ( '', qw/Owner HeldBy Contact CustomRole.{Licensee}/ );
+ is_deeply( [ $form->find_input('WatcherField')->possible_values ], \@watcher_options, 'WatcherField options' );
+
+ $m->submit_form_ok({
+ with_fields => {
+ WatcherField => 'CustomRole.{Licensee}',
+ ValueOfWatcher => 'licensee@example.com',
+ },
+ button => 'DoSearch',
+ }, "Search by role");
+
+ $m->text_contains('Some Software', 'search hit');
+ $m->text_lacks('More Software', 'search miss');
+
+ $m->follow_link_ok({ id => "assets-search" }, "Asset search link");
+ $m->submit_form_ok({ with_fields => { ValueOfCatalog => $catalog->Id }, button => 'AddClause' }, "Picked a catalog");
+ $m->submit_form_ok({
+ with_fields => {
+ WatcherField => 'CustomRole.{Licensee}',
+ ValueOfWatcher => 'licensee@example.com',
+ WatcherOp => 'NOT LIKE',
+ },
+ button => 'DoSearch',
+ }, "Search by role");
+
+ $m->text_lacks('Some Software', 'search miss');
+ $m->text_contains('More Software', 'search hit');
+}
+
+diag "Test permissions on Licensee";
+{
+ $m->logout;
+ $m->login('licensee@example.com', 'password');
+
+ $m->get_ok("$baseurl/Asset/Display.html?id=".$asset->Id);
+ $m->text_contains('Some Software', 'asset name shows on page');
+ $m->text_contains('Licensee', 'role name shows on page');
+
+ $m->get_ok("$baseurl/Asset/Display.html?id=".$asset2->Id);
+ $m->text_lacks('More Software', 'asset name does not show on page');
+ $m->text_lacks('Licensee', 'role name does not show on page');
+ $m->text_contains("You don't have permission to view this asset.");
+ $m->warning_like( qr/You don't have permission to view this asset/, 'got warning' );
+}
+
+$m->logout;
+$m->login; # log back in as root
+
+diag "Disable role";
+{
+ $m->follow_link_ok({ id => "admin-custom-roles-select" }, "Custom Role select link");
+ $m->follow_link_ok({ text => 'Licensee' }, "Picked a custom role");
+ $m->submit_form_ok({
+ with_fields => {
+ Enabled => 0,
+ },
+ }, "submitted update form");
+ $m->text_contains('Custom role disabled');
+
+ # refresh cache
+ RT::CustomRoles->RegisterRoles;
+
+ $role->Load($role->Id);
+ is $role->Name, "Licensee", "Name matches";
+ ok $role->Disabled, "now disabled";
+
+ my $catalog_id = $catalog->Id;
+ $catalog = RT::Catalog->new( RT->SystemUser );
+ $catalog->Load($catalog_id);
+ is_deeply([sort $catalog->Roles], [sort 'Contact', 'HeldBy', 'Owner'], '->Roles no longer includes Licensee');
+}
+
+diag "Test permissions on Licensee";
+{
+ $m->logout;
+ $m->login('licensee@example.com', 'password');
+
+ $m->get_ok("$baseurl/Asset/Display.html?id=".$asset->Id);
+ $m->text_lacks('Some Software', 'asset name does not show on page');
+ $m->text_lacks('Licensee', 'role name does not show on page');
+ $m->text_contains("You don't have permission to view this asset.");
+ $m->warning_like( qr/You don't have permission to view this asset/, 'got warning' );
+
+ $m->get_ok("$baseurl/Asset/Display.html?id=".$asset2->Id);
+ $m->text_lacks('More Software', 'asset name does not show on page');
+ $m->text_lacks('Licensee', 'role name does not show on page');
+ $m->text_contains("You don't have permission to view this asset.");
+ $m->warning_like( qr/You don't have permission to view this asset/, 'got warning' );
+}
+
+$m->logout;
+$m->login; # log back in as root
+
+diag "Create asset with disabled custom role";
+{
+ $m->follow_link_ok({ id => "assets-create" }, "Asset create link");
+ $m->submit_form_ok({ with_fields => { Catalog => $catalog->id, CatalogChanged => 1 } }, "Picked a catalog");
+ $m->text_lacks('Licensee', 'custom role name');
+ $m->text_lacks('Make sure user has real name set', 'custom role entry hint');
+
+ $m->submit_form_ok({
+ with_fields => {
+ id => 'new',
+ Name => 'All Software',
+ Owner => 'owner@example.com',
+ },
+ }, "submitted create form");
+ $m->text_like(qr/Asset .* created/, "Found created message");
+ my ($id) = $m->uri =~ /id=(\d+)/;
+
+ $asset3 = RT::Asset->new( RT->SystemUser );
+ $asset3->Load($id);
+ is $asset3->id, $id, "id matches";
+ is $asset3->Name, "All Software", "Name matches";
+ is $asset3->Owner->EmailAddress, 'owner@example.com', "Owner matches";
+ is $asset3->RoleAddresses($role->GroupType), '', "No Licensee";
+}
+
+done_testing;