[Rt-commit] rt branch, 4.6/core-group-management-extensions, created. rt-4.4.4-77-g0520c232f

Blaine Motsinger blaine at bestpractical.com
Thu May 30 20:46:24 EDT 2019


The branch, 4.6/core-group-management-extensions has been created
        at  0520c232fddd22de0b9c3b24eca505b4e0323138 (commit)

- Log -----------------------------------------------------------------
commit 5abeb826e88af03f6a13aa01d9428948c0bbfa0b
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu May 23 09:58:52 2019 -0500

    Core RT-Extension-GroupLinks

diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 19a41ef7a..b2877d132 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -74,7 +74,8 @@ use warnings;
 use base 'RT::Record';
 
 use Role::Basic 'with';
-with "RT::Record::Role::Rights";
+with "RT::Record::Role::Rights",
+     "RT::Record::Role::Links";
 
 sub Table {'Groups'}
 
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 5e9fbed9c..24d759cea 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -1077,9 +1077,15 @@ sub _BuildAdminMenu {
                 $page->child( basics         => title => loc('Basics'),       path => "/Admin/Groups/Modify.html?id=" . $obj->id );
                 $page->child( members        => title => loc('Members'),      path => "/Admin/Groups/Members.html?id=" . $obj->id );
                 $page->child( memberships    => title => loc('Memberships'),  path => "/Admin/Groups/Memberships.html?id=" . $obj->id );
+                $page->child( 'links'        => title => loc("Links"),        path => "/Admin/Groups/ModifyLinks.html?id=" . $obj->id,
+                    description => loc("Group links"),
+                );
                 $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Groups/GroupRights.html?id=" . $obj->id );
                 $page->child( 'user-rights'  => title => loc('User Rights'),  path => "/Admin/Groups/UserRights.html?id=" . $obj->id );
                 $page->child( history        => title => loc('History'),      path => "/Admin/Groups/History.html?id=" . $obj->id );
+                $page->child( 'summary'      => title => loc("Group Summary"), path => "/Group/Summary.html?id=" . $obj->id,
+                    description => loc("Group summary page"),
+                );
             }
         }
     }
diff --git a/share/html/Admin/Elements/AddLinks b/share/html/Admin/Elements/AddLinks
new file mode 100644
index 000000000..c4da372e1
--- /dev/null
+++ b/share/html/Admin/Elements/AddLinks
@@ -0,0 +1,39 @@
+<%args>
+$Object         => undef
+$CustomFields   => undef
+$ARGSRef        => $DECODED_ARGS
+</%args>
+<%init>
+my $id = ($Object and $Object->id)
+    ? $Object->id
+    : "new";
+
+my $exclude = qq| data-autocomplete="Groups" data-autocomplete-multiple="1" |;
+$exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
+</%init>
+% if (ref($Object) eq 'RT::Group') {
+<i><&|/l&>Enter names or IDs of other groups to link. Start typing a group name to see matching groups. Separate multiple entries with a comma.</&>
+% $m->callback( CallbackName => 'ExtraLinkInstructions' );
+</i><br />
+% } else {
+<i><&|/l&>Enter objects or URIs to link objects to. Separate multiple entries with spaces.</&></i><br />
+% }
+<table>
+  <tr>
+    <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
+    <td class="entry"><input name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude |n%>/></td>
+  </tr>
+  <tr>
+    <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
+    <td class="entry"> <input name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude |n%>/></td>
+  </tr>
+  <& /Elements/EditCustomFields,
+        Object          => $Object,
+        Grouping        => 'Links',
+        InTable         => 1,
+        ($CustomFields
+            ? (CustomFields => $CustomFields)
+            : ()),
+        &>
+% $m->callback( CallbackName => 'NewLink' );
+</table>
diff --git a/share/html/Admin/Elements/EditLinks b/share/html/Admin/Elements/EditLinks
new file mode 100644
index 000000000..20c030526
--- /dev/null
+++ b/share/html/Admin/Elements/EditLinks
@@ -0,0 +1,38 @@
+<table width="100%">
+  <tr>
+    <td valign="top" width="50%">
+      <h3><&|/l&>Current Links</&></h3>
+
+<table>
+  <tr>
+    <td class="labeltop"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
+    <td class="value">
+% while (my $link = $Object->RefersTo->Next) {
+      <& /Elements/EditLink, Link => $link, Mode => 'Target' &>
+%}
+    </td>
+  </tr>
+  <tr>
+    <td class="labeltop group-link edit-referredtoby"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
+    <td class="value group-link edit-referredtoby">
+% while (my $link = $Object->ReferredToBy->Next) {
+      <& /Elements/EditLink, Link => $link, Mode => 'Base' &>
+% }
+    </td>
+  </tr>
+  <tr>
+    <td></td>
+    <td><i><&|/l&>(Check box to remove link)</&></i></td>
+  </tr>
+</table>
+
+</td>
+<td valign="top">
+<h3><&|/l&>New Links</&></h3>
+<& AddLinks, %ARGS &>
+</td>
+</tr>
+</table>
+<%ARGS>
+$Object => undef
+</%ARGS>
diff --git a/share/html/Admin/Groups/ModifyLinks.html b/share/html/Admin/Groups/ModifyLinks.html
new file mode 100644
index 000000000..ce3b9cd9d
--- /dev/null
+++ b/share/html/Admin/Groups/ModifyLinks.html
@@ -0,0 +1,116 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => $title  &>
+
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+% $m->callback(CallbackName => 'BeforeActionList', Actions => \@results, ARGSRef => \%ARGS, GroupObj => $Group);
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<% RT->Config->Get('WebPath') %>/Admin/Groups/ModifyLinks.html" name="ModifyLinks" method="post">
+<input type="hidden" class="hidden" name="id" value="<%$Group->Id%>" />
+
+% $m->callback( CallbackName => 'FormStart', ARGSRef => \%ARGS );
+
+<&| /Widgets/TitleBox, title => loc('Manage Links for Group [_1]', $Group->Label) &>
+
+<& /Admin/Elements/EditLinks, Object => $Group &>
+</&>
+
+<& /Elements/Submit, Name => 'SubmitGroupLinks', Label => loc('Save Changes') &>
+</form>
+
+% $m->callback(CallbackName => 'AfterForm', ARGSRef => \%ARGS, GroupObj => $Group);
+
+<%INIT>
+my $Group = RT::Group->new($session{'CurrentUser'});
+$Group->Load($id) || Abort(loc('Could not load group'));
+my @results;
+
+$m->callback(CallbackName => 'Init', GroupObj => $Group, ARGSRef => \%ARGS, Results => \@results);
+
+my $title = loc("Modify Links for group [_1]", $Group->Label);
+
+if ( $ARGS{'SubmitGroupLinks'} ){
+
+    foreach my $link_type ( "RefersTo-$id", "$id-RefersTo" ){
+        next unless $ARGS{$link_type};
+
+        # List is comma delimited, which allows for group names with spaces
+        my @values = split ', ', $ARGS{$link_type};
+        foreach my $input ( @values ) {
+            if ( $input =~ /^\d+$/ ){
+                # Default scheme for link ids assumes a ticket. Since we're on the group
+                # links page, allow ids as input and prepend 'group:' here to
+                # create group links
+                $input = 'group:' . $input;
+                next;
+            }
+            else {
+                # Could be a group name. Try to look it up.
+                my $group = RT::Group->new($session{'CurrentUser'});
+                my ($ret, $msg) = $group->LoadUserDefinedGroup($input);
+                RT::Logger->info("Unable to load group from name $input: $msg") unless $ret;
+                $input = 'group:' . $group->Id if $ret and $group->Id;
+            }
+        }
+        $ARGS{$link_type} = join ' ', @values;
+    }
+
+    (@results) = ProcessRecordLinks(RecordObj => $Group, ARGSRef => \%ARGS);
+
+    MaybeRedirectForResults(
+        Actions     => \@results,
+        Arguments   => { id => $id },
+    );
+}
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/share/html/Elements/AddLinks b/share/html/Elements/AddLinks
index 30eb0e8bc..eb8787e9e 100644
--- a/share/html/Elements/AddLinks
+++ b/share/html/Elements/AddLinks
@@ -62,6 +62,8 @@ $exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
 <i><&|/l&>Enter tickets or URIs to link tickets to. Separate multiple entries with spaces.</&>
 <br /><&|/l&>You may enter links to Articles as "a:###", where ### represents the number of the Article.</&>
 <br /><&|/l&>Enter links to assets as "asset:###", where ### represents the asset ID.</&>
+<br /><&|/l&>Enter links to groups as "group:###", where ### represents the group ID.</&>
+<br /><&|/l&>Enter links to users as "user:###", where ### represents the user ID.</&>
 % $m->callback( CallbackName => 'ExtraLinkInstructions' );
 </i><br />
 % } elsif (ref($Object) eq 'RT::Queue') {
diff --git a/share/html/Elements/ShowPrincipal b/share/html/Elements/ShowPrincipal
index f8d1e0ed1..329266512 100644
--- a/share/html/Elements/ShowPrincipal
+++ b/share/html/Elements/ShowPrincipal
@@ -60,8 +60,9 @@ if ($Object->isa("RT::Group")) {
         map {+[($_->EmailAddress||''), $_]}
         @{ $Object->UserMembersObj( Recursively => 0 )->ItemsArrayRef };
 
-    # But don't link the groups
-    push @ret, sort map {$m->interp->apply_escapes( loc("Group: [_1]", $_->Name), 'h' )}
+    # Link to the group summary page
+    my $href = RT->Config->Get("WebPath") . "/Group/Summary.html?id=";
+    push @ret, sort map { "<a href=\"" . $href . $_->id . "\">" . loc("Group:") . " " . $_->Name . "</a>" }
         @{ $Object->GroupMembersObj( Recursively => 0)->ItemsArrayRef };
 
     $m->out( join($Separator, @ret) );

commit 79ba37d5126a25988a11995f6d0ca7e2762439fa
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 29 13:16:08 2019 -0500

    Core RT-Extension-GroupSummary

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 2559c662b..1a3619814 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1893,6 +1893,105 @@ Users also need the ModifySelf right to have access to this page.
 
 Set( $SelfServiceDownloadUserData, 0 );
 
+=back
+
+=head2 Group Summary Configuration
+
+Below are configuration options for the Group Summary page.
+
+=over
+
+=item C<$GroupSearchResultFormat>
+
+This controls the display of lists of users returned from the User
+Summary Search. The display of users in the Admin interface is
+controlled by C<%AdminSearchResultFormat>.
+
+=cut
+
+Set($GroupSearchResultFormat,
+         q{ '<a href="__WebPath__/Group/Summary.html?id=__id__">__id__</a>/TITLE:#'}
+        .q{,'<a href="__WebPath__/Group/Summary.html?id=__id__">__Name__</a>/TITLE:Name'}
+);
+
+=item C<@GroupSummaryPortlets>
+
+A list of portlets to be displayed on the Group Summary page.
+By default, we show all of the available portlets.
+Extensions may provide their own portlets for this page.
+
+=cut
+
+#Set(@GroupSummaryPortlets, (qw/ExtraInfo CreateTicket ActiveTickets InactiveTickets UserAssets /));
+Set(@GroupSummaryPortlets, (qw/ExtraInfo CreateTicket ActiveTickets InactiveTickets GroupAssets /));
+
+=item C<$GroupSummaryExtraInfo>
+
+This controls what information is displayed on the Group Summary
+portal. By default the group Name and Description are displayed
+
+=cut
+
+Set($GroupSummaryExtraInfo, "id, Name, Description");
+
+=item C<$GroupSummaryTicketListFormat>
+
+Control the appearance of the Active and Inactive ticket lists in the
+Group Summary.
+
+=cut
+
+Set($GroupSummaryTicketListFormat, q{
+       '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#',
+       '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject',
+       Status,
+       QueueName,
+       Owner,
+       Priority,
+       '__NEWLINE__',
+       '',
+       '<small>__Requestors__</small>',
+       '<small>__CreatedRelative__</small>',
+       '<small>__ToldRelative__</small>',
+       '<small>__LastUpdatedRelative__</small>',
+       '<small>__TimeLeft__</small>'
+});
+
+=item C<$GroupSearchFields>
+
+Specifies which fields of L<RT::Group> to match against and how to match
+each field when performing a quick search on groups.  Valid match
+methods are LIKE, STARTSWITH, ENDSWITH, =, and !=.  Valid search fields
+are id, Name, Description, or custom fields, which are specified as
+"CF.1234" or "CF.Name"
+
+=cut
+
+Set($GroupSearchFields, {
+    id          => '=',
+    Name        => 'LIKE',
+    Description => 'LIKE',
+});
+
+=item C<$AllowGroupAutocompleteForUnprivileged>
+
+Defines whether unprivileged users (users of SelfService) are allowed to
+autocomplete groups when searching. Setting this option to
+1 means unprivileged users will be able to search all your user created
+group names.
+
+=cut
+
+Set($AllowGroupAutocompleteForUnprivileged, 0);
+
+=item C<$HomepageComponents>
+
+C<$HomepageComponents> is an arrayref of allowed components on a
+user's customized homepage ("RT at a glance"). The GroupSummary extension
+provides a component called 'FindGroup' that you can add to your C<$HomepageComponents>
+in RT.
+
+=cut
 
 =back
 
diff --git a/lib/RT/Groups.pm b/lib/RT/Groups.pm
index 0d9784bc6..ae80e0e53 100644
--- a/lib/RT/Groups.pm
+++ b/lib/RT/Groups.pm
@@ -433,6 +433,110 @@ sub _DoSearch {
 
 }
 
+=head2 SimpleSearch
+
+Does a 'simple' search of Groups against a specified Term.
+
+This Term is compared to a number of fields using various types of SQL
+comparison operators.
+
+Ensures that the returned collection of Groups will have a value for Return.
+
+This method is passed the following.  You must specify a Term and a Return.
+
+    Fields     - Hashref of data - defaults to C<$GroupSearchFields> emulate that if you want to override
+    Term       - String that is in the fields specified by Fields
+    Return     - What field on the User you want to be sure isn't empty
+    Exclude    - Array reference of ids to exclude
+    Max        - Size to limit this collection to
+
+=cut
+
+sub SimpleSearch {
+    my $self = shift;
+    my %args = (
+        Fields      => RT->Config->Get('GroupSearchFields'),
+        Term        => undef,
+        Exclude     => [],
+        Return      => 'Name',
+        Max         => 10,
+        @_
+    );
+
+    return $self unless defined $args{Return}
+                        and defined $args{Term}
+                        and length $args{Term};
+
+    $self->RowsPerPage( $args{Max} );
+
+    $self->LimitToUserDefinedGroups();
+
+    while (my ($name, $op) = each %{$args{Fields}}) {
+        $op = 'STARTSWITH'
+            unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
+        if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
+            my $cfname = $1 || $2;
+            my $cf = RT::CustomField->new(RT->SystemUser);
+            my ($ok, $msg) = $cf->LoadByName( Name => $cfname, LookupType => 'RT::Group');
+            if ( $ok ) {
+                $self->LimitCustomField(
+                    CUSTOMFIELD     => $cf->Id,
+                    OPERATOR        => $op,
+                    VALUE           => $args{Term},
+                    ENTRYAGGREGATOR => 'OR',
+                    SUBCLAUSE       => 'autocomplete',
+                    CASESENSITIVE   => 0,
+                );
+            } else {
+                RT->Logger->warning("Asked to search custom field $name but unable to load a Group CF with the name $cfname: $msg");
+            }
+        } elsif ($name eq 'id' and $op =~ /(?:LIKE|(?:START|END)SWITH)$/i) {
+            $self->Limit(
+                FUNCTION        => "CAST( main.$name AS TEXT )",
+                OPERATOR        => $op,
+                VALUE           => $args{Term},
+                ENTRYAGGREGATOR => 'OR',
+                SUBCLAUSE       => 'autocomplete',
+                CASESENSITIVE   => 0,
+            ) if $args{Term} =~ /^\d+$/;
+        } else {
+            $self->Limit(
+                FIELD           => $name,
+                OPERATOR        => $op,
+                VALUE           => $args{Term},
+                ENTRYAGGREGATOR => 'OR',
+                SUBCLAUSE       => 'autocomplete',
+                CASESENSITIVE   => 0,
+            ) unless $args{Term} =~ /\D/ and $name eq 'id';
+        }
+    }
+
+    # Exclude groups we don't want
+    $self->Limit(FIELD => 'id', OPERATOR => 'NOT IN', VALUE => $args{Exclude} )
+        if @{$args{Exclude}};
+
+    if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+        $self->Limit(
+            FIELD    => $args{Return},
+            OPERATOR => 'IS NOT',
+            VALUE    => 'NULL',
+        );
+    }
+    else {
+        $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '', CASESENSITIVE => 0, );
+        $self->Limit(
+            FIELD           => $args{Return},
+            OPERATOR        => 'IS NOT',
+            VALUE           => 'NULL',
+            ENTRYAGGREGATOR => 'AND',
+            CASESENSITIVE   => 0,
+        );
+    }
+
+    return $self;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 24d759cea..d387455cf 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -167,6 +167,9 @@ sub BuildMainNav {
 
     $search->child( users => title => loc('Users'),   path => "/User/Search.html" );
 
+    $search->child( groups => title => loc('Groups'), path => "/Group/Search.html",
+        description => 'Group search' );
+
     $search->child( assets => title => loc("Assets"), path => "/Asset/Search/" )
         if $current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
 
@@ -1090,6 +1093,27 @@ sub _BuildAdminMenu {
         }
     }
 
+    if ( $request_path =~ m{^/Group/(Summary|History)\.html} ) {
+        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {          
+            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+            my $obj = RT::Group->new( $current_user );
+            $obj->Load($id);
+
+            if ( $obj and $obj->id and $current_user->HasRight(Right => 'SeeGroup', Object => $obj)) {
+                $page->child( basics         => title => loc('Basics'),       path => "/Admin/Groups/Modify.html?id=" . $obj->id );
+                $page->child( members        => title => loc('Members'),      path => "/Admin/Groups/Members.html?id=" . $obj->id );
+                $page->child( memberships    => title => loc('Memberships'),  path => "/Admin/Groups/Memberships.html?id=" . $obj->id );
+                $page->child( 'links' => title => loc("Links") => description => loc("Group links") =>
+                    path => "/Admin/Groups/ModifyLinks.html?id=" . $obj->id);
+                $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Groups/GroupRights.html?id=" . $obj->id );
+                $page->child( 'user-rights'  => title => loc('User Rights'),  path => "/Admin/Groups/UserRights.html?id=" . $obj->id );
+                $page->child( history        => title => loc('History'),      path => "/Admin/Groups/History.html?id=" . $obj->id );
+                $page->child( 'summary' => title => loc("Group Summary") => description => loc("Group summary page") =>
+                    path => "/Group/Summary.html?id=" . $obj->id);
+            }
+        }
+    }
+
     if ( $request_path =~ m{^/Admin/CustomFields/} ) {
         if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
             my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
diff --git a/share/html/Elements/FindGroup b/share/html/Elements/FindGroup
new file mode 100644
index 000000000..328b2a96e
--- /dev/null
+++ b/share/html/Elements/FindGroup
@@ -0,0 +1,50 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&|/Widgets/TitleBox, title => loc('Find a group')&>
+<& /Elements/GotoGroup &>
+</&>
diff --git a/share/html/Elements/GotoGroup b/share/html/Elements/GotoGroup
new file mode 100644
index 000000000..81dff29ee
--- /dev/null
+++ b/share/html/Elements/GotoGroup
@@ -0,0 +1,62 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<form name="GroupSearch" method="post" action="<% RT->Config->Get('WebPath') %>/Group/Search.html">
+<input type="text" name="GroupString" value="<% $Default %>" data-autocomplete="Groups" data-autocomplete-return="Name" id="autocomplete-GroupString" />
+<script type="text/javascript">
+jQuery(function(){
+    // Jump directly to the page if a group is chosen
+    jQuery("#autocomplete-GroupString").on("autocompleteselect", function( event, ui ) {
+        document.location = RT.Config.WebPath + "/Group/Summary.html?id=" + ui.item.id;
+    });
+});
+</script>
+<input type="submit" name="GroupSearch" value="<&|/l&>Search</&>" class="button" />
+</form>
+<%ARGS>
+$Default => ''
+</%ARGS>
diff --git a/share/html/Group/Elements/AssetList b/share/html/Group/Elements/AssetList
new file mode 100644
index 000000000..f8eb113a1
--- /dev/null
+++ b/share/html/Group/Elements/AssetList
@@ -0,0 +1,80 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%init>
+my $assets = RT::Assets->new($session{CurrentUser});
+$m->callback( CallbackName => 'ModifyAssetSearch', %ARGS, Assets => $assets, Roles => \@Roles );
+for my $role (@Roles) {
+    $assets->RoleLimit(
+        TYPE        => $role,
+        VALUE       => $Group->PrincipalId,
+        SUBCLAUSE   => "Role$role",
+    );
+}
+my $Format = q[
+    '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
+    '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
+    Description,
+];
+$m->callback( CallbackName => 'ModifyFormat', %ARGS, Format => \$Format );
+</%init>
+<&| /Widgets/TitleBox, title => $Title, class => "group asset-list" &>
+    <& /Elements/CollectionList,
+        Collection      => $assets,
+        OrderBy         => 'id',
+        Order           => 'ASC',
+        Format          => $Format,
+        AllowSorting    => 0,
+        HasResults      => $HasResults,
+        &>
+</&>
+<%args>
+$Group
+$Title
+ at Roles
+$HasResults => undef
+</%args>
diff --git a/share/html/Group/Elements/GroupInfo b/share/html/Group/Elements/GroupInfo
new file mode 100644
index 000000000..0e3e4aa2a
--- /dev/null
+++ b/share/html/Group/Elements/GroupInfo
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/ShowRecord,
+    Object      => $Group,
+    Format      => $format,
+    TrustFormat => 1, # Only modifiable by the RT server admin, so no need to scrub.
+    Class       => "$ClassPrefix-extra",
+    &>
+<%INIT>
+return unless blessed($Group) and $Group->id;
+return unless $FormatConfig;
+my $format = RT->Config->Get($FormatConfig);
+return unless $format;
+</%INIT>
+<%ARGS>
+$Group         => undef
+$FormatConfig => undef
+$ClassPrefix  => undef
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/ActiveTickets b/share/html/Group/Elements/Portlets/ActiveTickets
new file mode 100644
index 000000000..c5e31dec1
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/ActiveTickets
@@ -0,0 +1,68 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Group/Elements/TicketList ,
+    Group => $Group,
+    conditions => $conditions,
+    Rows => $Rows,
+    WatcherTypes => [qw(Watcher)],
+    Class => "group active-tickets",
+    Title => loc('Active Tickets'),
+    TitleBox => 1,
+    ShowHeader => 1,
+    Format => RT->Config->Get('GroupSummaryTicketListFormat'),
+&>
+<%INIT>
+unless ( @$conditions ) {
+    push @$conditions, { cond => "Status = '__Active__'" };
+}
+</%INIT>
+<%ARGS>
+$Group => undef
+$conditions => []
+$Rows => 10
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/CreateTicket b/share/html/Group/Elements/Portlets/CreateTicket
new file mode 100644
index 000000000..6fc68c036
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/CreateTicket
@@ -0,0 +1,58 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&| /Widgets/TitleBox, title => loc('Quick ticket creation'), class => "group create-ticket" &>
+<form action="<%RT->Config->Get('WebPath')%>/Ticket/Create.html">
+<&|/l&>Create a ticket with this group as Cc in Queue</&>
+<input type="hidden" name="AddGroupCc" value="<%$Group->Id%>">
+<& /Elements/SelectNewTicketQueue &>
+<input type="submit" value="<&|/l&>Create</&>">
+</form>
+</&>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/ExtraInfo b/share/html/Group/Elements/Portlets/ExtraInfo
new file mode 100644
index 000000000..f270cf3b5
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/ExtraInfo
@@ -0,0 +1,56 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<&| /Widgets/TitleBox, title => loc('Group Information'), class => "group extra-info" &>
+
+% $m->callback( Group => $Group, CallbackName => 'BeforeExtraInfo' );
+<& /Group/Elements/GroupInfo, Group => $Group, FormatConfig => 'GroupSummaryExtraInfo', ClassPrefix => 'group-summary' &>
+
+</&>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/GroupAssets b/share/html/Group/Elements/Portlets/GroupAssets
new file mode 100644
index 000000000..5ce5bdfd7
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/GroupAssets
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+%# Roles => [''] triggers the magical RoleLimit behavior that matches any role
+<& /Group/Elements/AssetList, Group => $Group, Roles => [''], Title => loc('Assigned Assets') &>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/InactiveTickets b/share/html/Group/Elements/Portlets/InactiveTickets
new file mode 100644
index 000000000..05c47e0d2
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/InactiveTickets
@@ -0,0 +1,68 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Group/Elements/TicketList ,
+    Group => $Group,
+    conditions => $conditions,
+    Rows => $Rows,
+    WatcherTypes => [qw(Watcher)],
+    Class => "group inactive-tickets",
+    Title => loc('Inactive Tickets'),
+    TitleBox => 1,
+    ShowHeader => 1,
+    Format => RT->Config->Get('GroupSummaryTicketListFormat'),
+&>
+<%INIT>
+unless ( @$conditions ) {
+    push @$conditions, { cond => "Status = '__Inactive__'" };
+}
+</%INIT>
+<%ARGS>
+$Group => undef
+$conditions => []
+$Rows => 10
+</%ARGS>
diff --git a/share/html/Group/Elements/TicketList b/share/html/Group/Elements/TicketList
new file mode 100644
index 000000000..29ef3b834
--- /dev/null
+++ b/share/html/Group/Elements/TicketList
@@ -0,0 +1,114 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+% if ( $TitleBox ) {
+    <& /Widgets/TitleBoxStart, title => $Title, title_href => $url, class => $Class &>
+% } else {
+    <span class="label"><a href="<% $url %>"><% $Title %>:</a></span>
+% }
+
+<& /Elements/CollectionList,
+    %QueryProperties,
+    Class          => 'RT::Tickets',
+    Page           => 1,
+    AllowSorting   => 0,
+    ShowNavigation => 0,
+&>
+
+% if ( $TitleBox ) {
+    <& /Widgets/TitleBoxEnd &>
+% }
+<%INIT>
+
+my $sql = '';
+
+$sql = join(' OR ', map { "$_.id = ".$Group->Id } @WatcherTypes );
+$sql = "( $sql )";
+
+$m->callback( CallbackName => 'ModifyWatcherSQL',
+        %ARGS,
+        sql => \$sql,
+);
+
+if (@$conditions) {
+    $sql .= " AND (".join( " OR ", map $_->{cond}, @$conditions).")";
+}
+
+my %QueryProperties = (
+    Query      => $sql,
+    OrderBy    => 'Priority|id',
+    Order      => 'DESC|DESC',
+    Rows       => $Rows || 10,
+    ShowHeader => $ShowHeader,
+    Format     => $Format,
+);
+
+$m->callback( CallbackName => 'ModifyQueryProperties',
+    %ARGS,
+    QueryProperties => \%QueryProperties,
+);
+
+my $url  = RT->Config->Get('WebPath') . '/Search/Results.html?';
+   $url .= $m->comp('/Elements/QueryString',
+                    Query       => $QueryProperties{'Query'},
+                    OrderBy     => $QueryProperties{'OrderBy'},
+                    Order       => $QueryProperties{'Order'},
+           );
+
+</%INIT>
+<%ARGS>
+$Title => ''
+$Class => ''
+ at WatcherTypes => (qw(Watcher))
+$Group => undef
+$conditions
+$Rows => 10
+$Description => ''
+$TitleBox => 0
+$Format => ''
+$ShowHeader => 0
+</%ARGS>
diff --git a/share/html/Group/Search.html b/share/html/Group/Search.html
new file mode 100644
index 000000000..072cd5bc3
--- /dev/null
+++ b/share/html/Group/Search.html
@@ -0,0 +1,100 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('Group Search'), Focus => '#autocomplete-GroupString' &>
+<& /Elements/Tabs &>
+    
+<& /Elements/GotoGroup, Default => $GroupString||'' &>
+
+<p> <&|/l&>This will search for groups by looking in the following fields:</&> <% $search_fields %></p>
+
+% if ($GroupString) {
+
+% unless ( $groups->Count ) {
+<p><&|/l&>No groups matching search criteria found.</&></p>
+% } else {
+<p><&|/l&>Select a group</&>:</p>
+
+<& /Elements/CollectionList,
+    OrderBy => 'Name',
+    Order => 'ASC',
+    Rows  => 100,
+    %ARGS,
+    Format => $Format,
+    Collection => $groups,
+    AllowSorting => 1,
+    PassArguments => [qw(Format Rows Page Order OrderBy GroupString)],
+&>
+
+% }
+% }
+
+<%INIT>
+my $groups;
+my $Format;
+if ( $GroupString ) {
+    $groups = RT::Groups->new($session{'CurrentUser'});
+    $groups->LimitToUserDefinedGroups();
+
+    $groups->SimpleSearch( Return    => 'Name',
+                           Term      => $GroupString,
+                           Max       => 100 );
+    my $first = $groups->First;
+    RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Group/Summary.html?id=".$first->Id)
+        if $groups->Count == 1;
+    $groups->GotoFirstItem;
+    $Format = RT->Config->Get('GroupSearchResultFormat');
+}
+
+my $search_fields = join ", ",
+  sort map {s/^CF\.(?:\{(.*)}|(.*))/$1 || $2/e; loc($_)}
+  keys %{RT->Config->Get('GroupSearchFields')};
+
+</%INIT>
+<%ARGS>
+$GroupString => undef
+</%ARGS>
diff --git a/share/html/Group/Summary.html b/share/html/Group/Summary.html
new file mode 100644
index 000000000..f3eceee1b
--- /dev/null
+++ b/share/html/Group/Summary.html
@@ -0,0 +1,103 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('Group: [_1]', $Group->Name) &>
+<& /Elements/Tabs &>
+
+<& /Elements/GotoGroup &>
+<& /Elements/ListActions, actions => \@results &>
+
+<%perl>
+$m->callback( CallbackName => 'BeforePortlets', ARGSRef => \%ARGS, Group => $Group, Portlets => $portlets );
+for my $portlet (@$portlets) {
+   $show_portlet->($portlet);
+}
+$m->callback( CallbackName => 'AfterPortlets', ARGSRef => \%ARGS, Group => $Group, Portlets => $portlets );
+</%perl>
+
+<%INIT>
+my $Group = RT::Group->new( $session{'CurrentUser'} );
+my ($status, $msg) = $Group->Load($id);
+unless ($status) {
+    RT->Logger->error("Unable to load group $id: $msg");
+    Abort("Unable to load Group $id");
+}
+
+unless ( $Group->CurrentUserHasRight('SeeGroup') ){
+    Abort("No permission to view group");
+}
+
+my @results;
+if ( $Group->Disabled ){
+    if ( $session{'CurrentUser'}->HasRight(
+        Object => RT->System, Right => 'AdminUsers' ) ){
+        push @results, loc('Group [_1] is currently disabled. Edit the group and check "Enabled" to enable.', $Group->Name);
+    }
+    else{
+        push @results, loc('Group [_1] is currently disabled.', $Group->Name);
+    }
+}
+
+my $portlets = RT->Config->Get('GroupSummaryPortlets');
+
+my $show_portlet = sub {
+    my $portlet = shift;
+    my $full_path = "/Group/Elements/Portlets/$portlet";
+    unless ( RT::Interface::Web->ComponentPathIsSafe($full_path) ) {
+        RT->Logger->error("unsafe portlet $portlet specified in GroupSummaryPortlets");
+        return;
+    }
+    unless ( $m->comp_exists($full_path) ) {
+        RT->Logger->error("Unable to find $portlet in /Group/Elements/Portlets - specified in GroupSummaryPortlets");
+        return;
+    }
+    $m->comp( $full_path, Group => $Group );
+};
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 025032046..47be144aa 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -56,7 +56,11 @@
   <input type="submit" name="SubmitTicket" value="Create" style="display:none">
   <input type="hidden" class="hidden" name="id" value="new" />
   <input type="hidden" class="hidden" name="Token" value="<% $ARGS{'Token'} %>" />
-  
+
+% if ( $ARGSRef->{'AddGroupCc'} ){
+<input type="hidden" class="hidden" name="AddGroupCc" value="<% $ARGSRef->{'AddGroupCc'} %>" />
+% }
+
 % $m->callback( CallbackName => 'FormStart', QueueObj => $QueueObj, ARGSRef => \%ARGS );
 
 % if ($gnupg_widget) {
@@ -525,4 +529,5 @@ $DependedOnBy => undef
 $MemberOf => undef
 $QuoteTransaction => undef
 $CloneTicket => undef
+$ARGSRef => undef
 </%ARGS>
diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 2681a3831..92fcac0d7 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -116,6 +116,7 @@ $TicketObj => undef
 $ShowHeaders => 0
 $HideUnsetFields => RT->Config->Get('HideUnsetFieldsOnDisplay', $session{CurrentUser})
 $ForceShowHistory => 0
+$ARGSRef => undef
 </%ARGS>
 
 <%INIT>
@@ -216,6 +217,26 @@ if ($ARGS{'id'} eq 'new') {
 
 $title = loc("#[_1]: [_2]", $TicketObj->Id, $TicketObj->Subject || '');
 
+if ( $ARGS{'id'} and $ARGS{'id'} eq 'new' ) {
+    if ( $ARGSRef->{'AddGroupCc'} ){
+        my $group = RT::Group->new($session{'CurrentUser'});
+        my ($ret, $msg) = $group->LoadUserDefinedGroup($ARGSRef->{'AddGroupCc'});
+
+        unless ( $ret ){
+            RT::Logger->warn("Unable to load group " . $ARGSRef->{'AddGroupCc'} . ", $msg. Not adding as Cc.");
+            return;
+        }
+
+        ( $ret, $msg ) = $$TicketObj->AddWatcher(
+            Type        => 'Cc',
+            PrincipalId => $group->Id
+        );
+
+        RT::Logger->warn("Unable to add group " . $group->Name . ": " . $group->Id . " as a Cc: $msg")
+            unless $ret;
+    }
+}
+
 $m->callback(
     CallbackName => 'BeforeDisplay',
     TicketObj => \$TicketObj,

commit dd273ca7a44f1963c6b7e381b494d0c7a6d754be
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed May 29 18:32:18 2019 -0500

    Core RT-Extension-GroupSelfService

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 1a3619814..12e5b17e4 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1893,6 +1893,26 @@ Users also need the ModifySelf right to have access to this page.
 
 Set( $SelfServiceDownloadUserData, 0 );
 
+=item C<$SelfServiceShowGroupTickets>
+
+Set this option to true to show a section with group tickets
+on self service pages.
+
+=cut
+
+Set($SelfServiceShowGroupTickets, 1);
+
+=item C<$GroupSelfServiceOwnerFilterName>
+
+To populate "My" tickets in Self Service based on Owner rather
+than Requestor, create a group CF called 'Group Type' and set
+it to the value you configure here. See the main documentation
+for more information.
+
+=cut
+
+Set($GroupSelfServiceOwnerFilterName, 'Partner');
+
 =back
 
 =head2 Group Summary Configuration
diff --git a/etc/initialdata b/etc/initialdata
index 74951a5b8..68604d6e9 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -940,4 +940,15 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
         Type              => 'Text',
         MaxValues         => 1,
     },
+    {
+        Name        => 'Group Type',
+        Description => 'Define a group type to sort by Owner in Self Service',
+        Type        => 'SelectSingle',
+        LookupType  => 'RT::Group',
+        RenderType  => 'Dropdown',
+        Values      => [
+            { Name => 'Partner', },
+            { Name => 'Customer', },
+        ],
+    },
 );
diff --git a/lib/RT/System.pm b/lib/RT/System.pm
index 05ef84738..d19be82c1 100644
--- a/lib/RT/System.pm
+++ b/lib/RT/System.pm
@@ -93,6 +93,7 @@ __PACKAGE__->AddRight( Staff   => ShowGlobalTemplates => 'Show global templates'
 __PACKAGE__->AddRight( General => LoadSavedSearch     => 'Allow loading of saved searches'); # loc
 __PACKAGE__->AddRight( General => CreateSavedSearch   => 'Allow creation of saved searches'); # loc
 __PACKAGE__->AddRight( Admin   => ExecuteCode         => 'Allow writing Perl code in templates, scrips, etc'); # loc
+__PACKAGE__->AddRight( Staff   => SeeSelfServiceGroupTicket => 'See tickets for other group members in SelfService' ); # loc
 
 =head2 AvailableRights
 
diff --git a/share/html/SelfService/Elements/AfterMyRequests b/share/html/SelfService/Elements/AfterMyRequests
new file mode 100644
index 000000000..f50de6a4a
--- /dev/null
+++ b/share/html/SelfService/Elements/AfterMyRequests
@@ -0,0 +1,12 @@
+<& /SelfService/Elements/MyGroupRequests,
+    %$ARGSRef,
+    status  => $Status,
+    title   => loc('My group\'s tickets'),
+    BaseURL => RT->Config->Get('WebPath') ."/SelfService/?",
+    Page    => $Page,
+&>
+<%args>
+$ARGSRef
+$Page => 1
+$Status => '__Active__'
+</%args>
diff --git a/share/html/SelfService/Elements/BeforeMyRequests b/share/html/SelfService/Elements/BeforeMyRequests
new file mode 100644
index 000000000..0858164ec
--- /dev/null
+++ b/share/html/SelfService/Elements/BeforeMyRequests
@@ -0,0 +1,27 @@
+<%init>
+# Find current user's groups and determine if they are a partner
+# Load a system user to see all groups without a rights check on whether
+# the current user has ShowGroup.
+my $user = RT::User->new(RT->SystemUser);
+my ($ret, $msg) = $user->Load($session{'CurrentUser'}->Id);
+unless ( $ret ){
+    RT::Logger->error("Unable to load user record for user: " . $session{'CurrentUser'}->Name . " :$msg");
+    return;
+}
+my $groups_obj = $user->OwnGroups;
+my $is_partner;
+
+while ( my $group = $groups_obj->Next ){
+    if ( $group
+        and $group->FirstCustomFieldValue("Group Type")
+        and $group->FirstCustomFieldValue("Group Type") eq RT->Config->Get('GroupSelfServiceOwnerFilterName') ){
+        $is_partner = 1;
+        last;
+    }
+}
+
+$ARGSRef->{'SortByRole'} = 'Owner' if $is_partner;
+</%init>
+<%args>
+$ARGSRef
+</%args>
diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyGroupRequests
similarity index 65%
copy from share/html/SelfService/Elements/MyRequests
copy to share/html/SelfService/Elements/MyGroupRequests
index 0cd4e3781..2011ea519 100644
--- a/share/html/SelfService/Elements/MyRequests
+++ b/share/html/SelfService/Elements/MyGroupRequests
@@ -2,7 +2,7 @@
 %#
 %# COPYRIGHT:
 %#
-%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
 %#                                          <sales at bestpractical.com>
 %#
 %# (Except where explicitly superseded by other copyright notices)
@@ -59,9 +59,47 @@
 </&>
 
 <%INIT>
+
+unless ( RT->Config->Get('SelfServiceShowGroupTickets')
+         and $session{'CurrentUser'}->HasRight(Right => 'SeeSelfServiceGroupTicket', Object => $RT::System) ){
+    return;
+}
+
 $title ||= loc("My [_1] tickets", $friendly_status);
-my $id = $session{'CurrentUser'}->id;
-my $Query = "( Watcher.id = $id )";
+
+# Load a system user to see all groups without a rights check on whether
+# the current user has ShowGroup.
+my $user = RT::User->new(RT->SystemUser);
+my ($ret, $msg) = $user->Load($session{'CurrentUser'}->Id);
+unless ( $ret ){
+    RT::Logger->error("Unable to load user record for user: " . $session{'CurrentUser'}->Name . " :$msg");
+    return;
+}
+my $groups_obj = $user->OwnGroups;
+
+my $Query = '';
+
+if ( $groups_obj->Count ){
+    my $group = $groups_obj->Next;
+
+    # Confirm we got a group. Count can report available groups, but
+    # if the current user doesn't have SeeGroup, it won't be loaded.
+    if ( $group ){
+        $Query = "(( WatcherGroup = " . $group->Id . " )";
+    }
+
+    # Handle multiple groups
+    while ( $group = $groups_obj->Next ){
+        $Query .= " OR ( WatcherGroup = " . $group->Id . " )";
+    }
+
+    $Query .= ")" if $Query;
+}
+
+# Exclude tickets where current user is requestor or cc since they will
+# appear in the My open tickets list
+$Query .= " AND" if $Query;
+$Query .= " $SortByRole.id != " . $session{'CurrentUser'}->Id;
 
 if ($status) {
     $status =~ s/(['\\])/\\$1/g;
@@ -72,11 +110,12 @@ my $Format = RT->Config->Get('DefaultSelfServiceSearchResultFormat');
 </%INIT>
 <%ARGS>
 $title => undef
-$friendly_status => loc('open')
+$friendly_status => loc("group's")
 $status => undef
 $BaseURL => undef
 $Page => 1
 @Order => ('ASC')
 @OrderBy => ('Created')
 $Rows => 50
+$SortByRole => 'Requestor' # Role to use when determining "My" tickets
 </%ARGS>
diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyRequests
index 0cd4e3781..4c05189eb 100644
--- a/share/html/SelfService/Elements/MyRequests
+++ b/share/html/SelfService/Elements/MyRequests
@@ -61,7 +61,7 @@
 <%INIT>
 $title ||= loc("My [_1] tickets", $friendly_status);
 my $id = $session{'CurrentUser'}->id;
-my $Query = "( Watcher.id = $id )";
+my $Query = "( $SortByRole.id = $id OR Watcher.id = $id )";
 
 if ($status) {
     $status =~ s/(['\\])/\\$1/g;
@@ -79,4 +79,5 @@ $Page => 1
 @Order => ('ASC')
 @OrderBy => ('Created')
 $Rows => 50
+$SortByRole => 'Requestor' # Role to use when determining "My" tickets
 </%ARGS>

commit 0520c232fddd22de0b9c3b24eca505b4e0323138
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu May 30 19:30:12 2019 -0500

    Fix test for initialdata change
    
    Use the custom field id created through the test instead of the
    previously hardcoded value.

diff --git a/t/web/cf_image.t b/t/web/cf_image.t
index 7c294b847..6880cd416 100644
--- a/t/web/cf_image.t
+++ b/t/web/cf_image.t
@@ -20,8 +20,8 @@ my $cfid = $m->form_name('ModifyCustomField')->value('id');
 ok $cfid, "Created CF correctly";
 
 $m->follow_link_ok( {id => "page-applies-to"} );
-$m->form_with_fields( "AddCustomField-2" );
-$m->tick( "AddCustomField-2", 0 );
+$m->form_with_fields( "AddCustomField-$cfid" );
+$m->tick( "AddCustomField-$cfid", 0 );
 $m->click_ok( "UpdateObjs" );
 $m->content_contains("Globally added custom field Images");
 
@@ -46,7 +46,7 @@ $m->content_contains("Upload one image");
 $m->submit_form_ok({
     form_name => "TicketModify",
     fields    => {
-        "Object-RT::Ticket-1-CustomField-2-Upload" =>
+        "Object-RT::Ticket-1-CustomField-$cfid-Upload" =>
             RT::Test::get_relocatable_file('bpslogo.png', '..', 'data'),
     },
 });
@@ -54,7 +54,7 @@ $m->content_contains("bpslogo.png added");
 $m->content_contains("/Download/CustomFieldValue/1/bpslogo.png");
 
 $m->form_name("TicketModify");
-$m->tick("Object-RT::Ticket-1-CustomField-2-DeleteValueIds", 1);
+$m->tick("Object-RT::Ticket-1-CustomField-$cfid-DeleteValueIds", 1);
 $m->click_ok("SubmitTicket");
 $m->content_lacks("/Download/CustomFieldValue/1/bpslogo.png");
 

-----------------------------------------------------------------------


More information about the rt-commit mailing list