[Bps-public-commit] rt-extension-automaticassignment branch, master, updated. 285aaf75b68e8ec55a04814acfd976bce737ab19

Shawn Moore shawn at bestpractical.com
Wed Aug 10 20:19:46 EDT 2016


The branch, master has been updated
       via  285aaf75b68e8ec55a04814acfd976bce737ab19 (commit)
       via  00d1435f28e320753c824e5fe82e166f120667f0 (commit)
       via  d2fc5c8de521e01006865143793dd27df7b81a65 (commit)
       via  d3034a0ed6cb28e45aff2220e81c019714920886 (commit)
       via  8bd250a59907d84f0f1615f48a4b7b2ec0197a37 (commit)
       via  30a77297a73a6a855f55cf5593f2d18831b6738a (commit)
      from  788f5043b06aef1b95847cebe4a762d29b762ae0 (commit)

Summary of changes:
 README                                             |   2 +
 etc/AutomaticAssignment_Config.pm                  |  16 +++
 html/Admin/Global/AutomaticAssignment.html         |  27 ----
 html/Admin/Queues/AutomaticAssignment.html         | 139 +++++++++++++++++++--
 html/Admin/Queues/Elements/Chooser/Random          |   7 +-
 html/Admin/Queues/Elements/Chooser/RoundRobin      |   7 +-
 html/Admin/Queues/Elements/Chooser/TicketStatus    |   9 +-
 html/Admin/Queues/Elements/Chooser/TimeLeft        |   7 +-
 html/Admin/Queues/Elements/Filter/BusinessHours    |   9 --
 html/Admin/Queues/Elements/Filter/ExcludedDates    |  39 +++++-
 html/Admin/Queues/Elements/Filter/MemberOfGroup    |  27 +++-
 html/Admin/Queues/Elements/Filter/MemberOfRole     |  33 ++++-
 html/Admin/Queues/Elements/Filter/WorkSchedule     |  31 +++++
 html/Admin/Queues/Elements/SortableBox             |  27 ++++
 .../AutomaticAssignment/Elements/Tabs/Privileged   |  10 --
 html/Helpers/AddFilter                             |  16 +++
 html/Helpers/SelectChooser                         |  15 +++
 lib/RT/Extension/AutomaticAssignment.pm            |  42 +++----
 lib/RT/Extension/AutomaticAssignment/Chooser.pm    |  12 ++
 .../AutomaticAssignment/Chooser/Random.pm          |   2 +
 .../AutomaticAssignment/Chooser/RoundRobin.pm      |   5 +-
 .../AutomaticAssignment/Chooser/TicketStatus.pm    |   2 +
 .../AutomaticAssignment/Chooser/TimeLeft.pm        |   3 +-
 lib/RT/Extension/AutomaticAssignment/Filter.pm     |  13 +-
 .../AutomaticAssignment/Filter/ExcludedDates.pm    |  42 ++++---
 .../AutomaticAssignment/Filter/MemberOfGroup.pm    |  27 ++--
 .../AutomaticAssignment/Filter/MemberOfRole.pm     |  14 +++
 .../Filter/{BusinessHours.pm => WorkSchedule.pm}   |  26 ++--
 static/css/automatic-assignment.css                |  22 ++++
 static/js/automatic-assignment.js                  |  66 ++++++++++
 30 files changed, 563 insertions(+), 134 deletions(-)
 create mode 100644 etc/AutomaticAssignment_Config.pm
 delete mode 100644 html/Admin/Global/AutomaticAssignment.html
 delete mode 100644 html/Admin/Queues/Elements/Filter/BusinessHours
 create mode 100644 html/Admin/Queues/Elements/Filter/WorkSchedule
 create mode 100644 html/Admin/Queues/Elements/SortableBox
 delete mode 100644 html/Callbacks/AutomaticAssignment/Elements/Tabs/Privileged
 create mode 100644 html/Helpers/AddFilter
 create mode 100644 html/Helpers/SelectChooser
 rename lib/RT/Extension/AutomaticAssignment/Filter/{BusinessHours.pm => WorkSchedule.pm} (77%)
 create mode 100644 static/js/automatic-assignment.js

- Log -----------------------------------------------------------------
commit 30a77297a73a6a855f55cf5593f2d18831b6738a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Aug 10 21:06:20 2016 +0000

    Remove global defaults from automatic assignment
    
        It's hard to opt out of global defaults in a specific queue, and also
        it's easy enough to configure new queues with the admin UI

diff --git a/README b/README
index f5d4375..338faa0 100644
--- a/README
+++ b/README
@@ -27,6 +27,8 @@ INSTALLATION
         Automatic Reassignment, the automatic assignment will happen even if
         the ticket has an owner already.
 
+    Configure automatic assignment policies
+
 AUTHOR
     Best Practical Solutions, LLC <modules at bestpractical.com>
 
diff --git a/html/Admin/Global/AutomaticAssignment.html b/html/Admin/Global/AutomaticAssignment.html
deleted file mode 100644
index 311346f..0000000
--- a/html/Admin/Global/AutomaticAssignment.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<& /Elements/Header, Title => $title &>
-<& /Elements/Tabs &>
-<& /Elements/ListActions, actions => \@results &>
-
-<form method="post" class="automatic-assignment" action="AutomaticAssignment.html">
-
-<h1>Filters</h1>
-
-<& /Admin/Queues/Elements/Filter/BusinessHours, i => 0 &>
-<& /Admin/Queues/Elements/Filter/ExcludedDates, i => 1 &>
-<& /Admin/Queues/Elements/Filter/MemberOfGroup, i => 2 &>
-<& /Admin/Queues/Elements/Filter/MemberOfRole, i => 3 &>
-
-<h1>Chooser</h1>
-
-<& /Admin/Queues/Elements/Chooser/Random &>
-<& /Admin/Queues/Elements/Chooser/RoundRobin &>
-<& /Admin/Queues/Elements/Chooser/TicketStatus &>
-<& /Admin/Queues/Elements/Chooser/TimeLeft &>
-
-</form>
-
-<%INIT>
-my @results;
-
-my $title = loc('Automatic Assignment global defaults');
-</%INIT>
diff --git a/html/Callbacks/AutomaticAssignment/Elements/Tabs/Privileged b/html/Callbacks/AutomaticAssignment/Elements/Tabs/Privileged
deleted file mode 100644
index 2bccb39..0000000
--- a/html/Callbacks/AutomaticAssignment/Elements/Tabs/Privileged
+++ /dev/null
@@ -1,10 +0,0 @@
-<%INIT>
-return unless Menu->child('admin')
-           && Menu->child('admin')->child('global');
-
-Menu->child('admin')->child('global')->child(
-    'automaticassignment' =>
-    title => loc('Automatic Assignment'),
-    path => "/Admin/Global/AutomaticAssignment.html",
-);
-</%INIT>
diff --git a/lib/RT/Extension/AutomaticAssignment.pm b/lib/RT/Extension/AutomaticAssignment.pm
index c509756..184716e 100644
--- a/lib/RT/Extension/AutomaticAssignment.pm
+++ b/lib/RT/Extension/AutomaticAssignment.pm
@@ -94,10 +94,7 @@ sub _ConfigForTicket {
         return;
     }
 
-    # merge the queue-specific config into the default config
-    my %merged_config = %{ $config->{Default} || {} };
-    $merged_config{ $_ } = $config->{QueueDefault}{ $queue }->{ $_ }
-        for keys %{ $config->{QueueDefault}{ $queue } || {} };
+    my %merged_config = %{ $config->{Queue}{ $queue } || {} };
 
     # filters not required, since the default list is "users who can own
     # tickets in this queue"
@@ -206,17 +203,6 @@ will happen even if the ticket has an owner already.
 
 =item Configure automatic assignment policies
 
-    Set(%AutomaticAssignment_Choosers, (
-        Default => 'Random',
-        QueueDefaults => {
-            General => 'TicketStatus',
-            Review => {
-                class => 'TicketStatus',
-                ties => [ ['new', 'open'], 'stalled' ],
-            },
-        },
-    ));
-
 =back
 
 =head1 AUTHOR

commit 8bd250a59907d84f0f1615f48a4b7b2ec0197a37
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Aug 10 21:09:30 2016 +0000

    Switch from RT->Config to an attribute so admin UI can modify configuration

diff --git a/lib/RT/Extension/AutomaticAssignment.pm b/lib/RT/Extension/AutomaticAssignment.pm
index 184716e..d4d3f63 100644
--- a/lib/RT/Extension/AutomaticAssignment.pm
+++ b/lib/RT/Extension/AutomaticAssignment.pm
@@ -87,27 +87,27 @@ sub _ConfigForTicket {
     my $self = shift;
     my $ticket = shift;
 
-    my $queue = $ticket->QueueObj->Name;
-    my $config = RT->Config->Get('AutomaticAssignment');
-    if (!$config) {
+    my $queue = $ticket->QueueObj;
+    my $attr = $queue->FirstAttribute('AutomaticAssignment');
+    if (!$attr || !$attr->Content) {
         RT->Logger->error("No AutomaticAssignment config defined; automatic assignment cannot occur.");
         return;
     }
 
-    my %merged_config = %{ $config->{Queue}{ $queue } || {} };
+    my $config = $attr->Content;
 
     # filters not required, since the default list is "users who can own
     # tickets in this queue"
-    $merged_config{filters} ||= [];
+    $config->{filters} ||= [];
 
     # chooser is required
-    if (!$merged_config{chooser}) {
+    if (!$config->{chooser}) {
         RT->Logger->error("No AutomaticAssignment chooser defined for queue '$queue'; automatic assignment cannot occur.");
         return;
     }
 
     # load each filter class
-    for (@{ $merged_config{filters} }) {
+    for (@{ $config->{filters} }) {
         if (!ref($_)) {
             $_ = {
                 class => $self->_LoadedClass('Filter', $_),
@@ -122,16 +122,16 @@ sub _ConfigForTicket {
     }
 
     # load chooser class
-    if (!ref($merged_config{chooser})) {
-        $merged_config{chooser} = {
-            class => $self->_LoadedClass('Chooser', $merged_config{chooser}),
+    if (!ref($config->{chooser})) {
+        $config->{chooser} = {
+            class => $self->_LoadedClass('Chooser', $config->{chooser}),
         };
     }
     else {
-        $merged_config{chooser}{class} = $self->_LoadedClass('Chooser', $merged_config{chooser}{class});
+        $config->{chooser}{class} = $self->_LoadedClass('Chooser', $config->{chooser}{class});
     }
 
-    return \%merged_config;
+    return $config;
 }
 
 sub OwnerForTicket {

commit d3034a0ed6cb28e45aff2220e81c019714920886
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Aug 10 21:10:06 2016 +0000

    Filter and Chooser form WIP

diff --git a/html/Admin/Queues/Elements/Chooser/Random b/html/Admin/Queues/Elements/Chooser/Random
index 095e4c9..71bac07 100644
--- a/html/Admin/Queues/Elements/Chooser/Random
+++ b/html/Admin/Queues/Elements/Chooser/Random
@@ -1,3 +1,6 @@
 <&| /Widgets/TitleBox, title => loc('Random') &>
 <p>This chooser selects a random owner from the pool of eligible users.</p>
 </&>
+<%ARGS>
+$prefix
+</%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/RoundRobin b/html/Admin/Queues/Elements/Chooser/RoundRobin
index 53a4f2c..3eef502 100644
--- a/html/Admin/Queues/Elements/Chooser/RoundRobin
+++ b/html/Admin/Queues/Elements/Chooser/RoundRobin
@@ -1,3 +1,6 @@
 <&| /Widgets/TitleBox, title => loc('Round Robin') &>
 <p>This chooser selects the user for whom it has been the longest amount of timesince they were last automatically assigned a ticket in the same queue. This effectively distributes the tickets evenly between the userbase in a round robin fashion, even if users enter and leave the pool.</p>
 </&>
+<%ARGS>
+$prefix
+</%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TicketStatus b/html/Admin/Queues/Elements/Chooser/TicketStatus
index 37c7132..43d8137 100644
--- a/html/Admin/Queues/Elements/Chooser/TicketStatus
+++ b/html/Admin/Queues/Elements/Chooser/TicketStatus
@@ -1,3 +1,6 @@
 <&| /Widgets/TitleBox, title => loc('Ticket Status') &>
-<p>This chooser selects the user with the fewest number of active tickets. If multiple users have the same number of tickets, then the ticket is assigned randomly.</p>
+<p>This chooser selects the user with the fewest number of active tickets. If multiple users have the same number of tickets, then the ticket is assigned randomly to one of them.</p>
 </&>
+<%ARGS>
+$prefix
+</%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TimeLeft b/html/Admin/Queues/Elements/Chooser/TimeLeft
index 41821dc..5145321 100644
--- a/html/Admin/Queues/Elements/Chooser/TimeLeft
+++ b/html/Admin/Queues/Elements/Chooser/TimeLeft
@@ -1,3 +1,6 @@
 <&| /Widgets/TitleBox, title => loc('Time Left') &>
 <p>This chooser selects the user with the least total amount of Time Left on their tickets in the given queue. If a particular ticket does not have Time Left, then that ticket's Time Estimated minus its Time Worked is used instead.</p> 
 </&>
+<%ARGS>
+$prefix
+</%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/BusinessHours b/html/Admin/Queues/Elements/Filter/BusinessHours
index 0535604..a804265 100644
--- a/html/Admin/Queues/Elements/Filter/BusinessHours
+++ b/html/Admin/Queues/Elements/Filter/BusinessHours
@@ -1,9 +1,29 @@
 <&| /Widgets/TitleBox, title => loc('Business Hours') &>
 <p>This filter selects eligible owners by their work schedule. The following user custom field decides which work schedule each user maintains. Administrators may update the <tt>%ServiceBusinessHours</tt> RT config for adding or modifying the available schedules.</p>
-<% $i %>
+
+% if ($user_cfs->Count) {
+<table>
+<tr>
+<td class="label"><label for="<% $prefix %>_user_cf">Custom Field:</label></td>
+<td class="value">
+<select id="<% $prefix %>_user_cf" name="<% $prefix %>_user_cf">
+<option value="">-</option>
+% while (my $cf = $user_cfs->Next) {
+<option value="<% $cf->Id %>"><% $cf->Name %></option>
+% }
+</select>
+</td>
+</tr>
+</table>
+% } else {
+<p>No user custom fields found.</p>
+% }
 </&>
 <%INIT>
+my $user_cfs = RT::CustomFields->new($session{CurrentUser});
+$user_cfs->LimitToLookupType(RT::User->CustomFieldLookupType);
+$user_cfs->ApplySortOrder;
 </%INIT>
 <%ARGS>
-$i
+$prefix
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/ExcludedDates b/html/Admin/Queues/Elements/Filter/ExcludedDates
index 219891a..05b55d9 100644
--- a/html/Admin/Queues/Elements/Filter/ExcludedDates
+++ b/html/Admin/Queues/Elements/Filter/ExcludedDates
@@ -1,9 +1,40 @@
 <&| /Widgets/TitleBox, title => loc('Excluded Dates') &>
 <p>This filter selects eligible owners by datetime custom fields on users, meant for scheduled vacations. If the current date and time falls between the following two custom fields, the user will be excluded from automatic assignment.</p>
-<% $i %>
+
+% if ($user_cfs->Count) {
+<table>
+<tr>
+<td class="label"><label for="<% $prefix %>_begin">Begin Custom Field:</label></td>
+<td class="value">
+<select id="<% $prefix %>_begin" name="<% $prefix %>_begin">
+<option value="">-</option>
+% while (my $cf = $user_cfs->Next) {
+<option value="<% $cf->Id %>"><% $cf->Name %></option>
+% }
+</select>
+</td>
+</tr>
+<tr>
+<td class="label"><label for="<% $prefix %>_end">End Custom Field:</label></td>
+<td class="value">
+<select id="<% $prefix %>_end" name="<% $prefix %>_end">
+<option value="">-</option>
+% while (my $cf = $user_cfs->Next) {
+<option value="<% $cf->Id %>"><% $cf->Name %></option>
+% }
+</select>
+</td>
+</tr>
+</table>
+% } else {
+<p>No user custom fields found.</p>
+% }
 </&>
 <%INIT>
+my $user_cfs = RT::CustomFields->new($session{CurrentUser});
+$user_cfs->LimitToLookupType(RT::User->CustomFieldLookupType);
+$user_cfs->ApplySortOrder;
 </%INIT>
 <%ARGS>
-$i
+$prefix
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfGroup b/html/Admin/Queues/Elements/Filter/MemberOfGroup
index 458aefb..3e3175f 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfGroup
+++ b/html/Admin/Queues/Elements/Filter/MemberOfGroup
@@ -1,9 +1,28 @@
 <&| /Widgets/TitleBox, title => loc('Member of Group') &>
 <p>This filter selects eligible owners by their group membership. Only members of the following group will be automatically assigned tickets.</p>
-<% $i %>
+
+% if ($groups->Count) {
+<table>
+<tr>
+<td class="label"><label for="<% $prefix %>_group">Group:</label></td>
+<td class="value">
+<select id="<% $prefix %>_group" name="<% $prefix %>_group">
+<option value="">-</option>
+% while (my $group = $groups->Next) {
+<option value="<% $group->Id %>"><% $group->Name %></option>
+% }
+</select>
+</td>
+</tr>
+</table>
+% } else {
+<p>No groups found.</p>
+% }
 </&>
 <%INIT>
+my $groups = RT::Groups->new($session{CurrentUser});
+$groups->LimitToUserDefinedGroups;
 </%INIT>
 <%ARGS>
-$i
+$prefix
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfRole b/html/Admin/Queues/Elements/Filter/MemberOfRole
index 7398fc8..500646a 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfRole
+++ b/html/Admin/Queues/Elements/Filter/MemberOfRole
@@ -1,9 +1,33 @@
 <&| /Widgets/TitleBox, title => loc('Member of Role') &>
 <p>This filter selects eligible owners by their role membership. Only members of the following role, either on the queue or on the ticket itself, will be automatically assigned tickets.</p>
-<% $i %>
+
+<table>
+<tr>
+<td class="label"><label for="<% $prefix %>_role">Role:</label></td>
+<td class="value">
+<select id="<% $prefix %>_role" name="<% $prefix %>_role">
+<option value="">-</option>
+% for my $role (qw/AdminCc Cc Requestor/) {
+<option value="<% $role %>"><% $role %></option>
+% }
+% if ($custom_roles) {
+% while (my $role = $custom_roles->Next) {
+<option value="<% $role->Id %>"><% $role->Name %></option>
+% }
+% }
+</select>
+</td>
+</tr>
+</table>
 </&>
+
 <%INIT>
+my $custom_roles;
+if ( RT::Handle::cmp_version($RT::VERSION,'4.4.0') >= 0 ) {
+    $custom_roles = RT::CustomRoles->new($session{CurrentUser});
+    $custom_roles->UnLimit;
+}
 </%INIT>
 <%ARGS>
-$i
+$prefix
 </%ARGS>

commit d2fc5c8de521e01006865143793dd27df7b81a65
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Aug 10 21:30:36 2016 +0000

    WIP

diff --git a/html/Admin/Queues/AutomaticAssignment.html b/html/Admin/Queues/AutomaticAssignment.html
index 7b860f7..ebd6825 100644
--- a/html/Admin/Queues/AutomaticAssignment.html
+++ b/html/Admin/Queues/AutomaticAssignment.html
@@ -7,18 +7,28 @@
 
 <h1>Filters</h1>
 
-<& /Admin/Queues/Elements/Filter/BusinessHours, i => 0 &>
-<& /Admin/Queues/Elements/Filter/ExcludedDates, i => 1 &>
-<& /Admin/Queues/Elements/Filter/MemberOfGroup, i => 2 &>
-<& /Admin/Queues/Elements/Filter/MemberOfRole, i => 3 &>
+% my $i = 0;
+% for my $filter (@{ $config->{filters} }) {
+%    ++$i;
+%    my $name = "BusinessHours";
+%    my $path = "/Admin/Queues/Elements/Filter/$name";
+%    my $prefix = "Filter_${name}_$i";
+
+%    $m->comp($path, prefix => $prefix, config => $filter);
+% }
+
+<select>
+<option value="">-</option>
+<option value="BusinessHours">Business Hours</option>
+<option value="ExcludedDates">Excluded Dates</option>
+<option value="MemberOfGroup">Member of Group</option>
+<option value="MemberOfRole">Member of Role</option>
+
+</select>
+<input type="submit" class="button" value="Add Filter">
 
 <h1>Chooser</h1>
 
-<& /Admin/Queues/Elements/Chooser/Random &>
-<& /Admin/Queues/Elements/Chooser/RoundRobin &>
-<& /Admin/Queues/Elements/Chooser/TicketStatus &>
-<& /Admin/Queues/Elements/Chooser/TimeLeft &>
-
 </form>
 
 <%INIT>
@@ -27,6 +37,9 @@ my @results;
 my $QueueObj = RT::Queue->new($session{'CurrentUser'});
 $QueueObj->Load($id) || Abort(loc("Couldn't load queue", $id));
 
+my $attr = $QueueObj->FirstAttribute('AutomaticAssignment');
+my $config = $attr ? $attr->Content : {};
+
 my $title = loc('Automatic Assignment for queue [_1]', $QueueObj->Name);
 </%INIT>
 <%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/Random b/html/Admin/Queues/Elements/Chooser/Random
index 71bac07..d9f6939 100644
--- a/html/Admin/Queues/Elements/Chooser/Random
+++ b/html/Admin/Queues/Elements/Chooser/Random
@@ -3,4 +3,5 @@
 </&>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/RoundRobin b/html/Admin/Queues/Elements/Chooser/RoundRobin
index 3eef502..320633f 100644
--- a/html/Admin/Queues/Elements/Chooser/RoundRobin
+++ b/html/Admin/Queues/Elements/Chooser/RoundRobin
@@ -3,4 +3,5 @@
 </&>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TicketStatus b/html/Admin/Queues/Elements/Chooser/TicketStatus
index 43d8137..ff24381 100644
--- a/html/Admin/Queues/Elements/Chooser/TicketStatus
+++ b/html/Admin/Queues/Elements/Chooser/TicketStatus
@@ -3,4 +3,5 @@
 </&>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TimeLeft b/html/Admin/Queues/Elements/Chooser/TimeLeft
index 5145321..2536285 100644
--- a/html/Admin/Queues/Elements/Chooser/TimeLeft
+++ b/html/Admin/Queues/Elements/Chooser/TimeLeft
@@ -3,4 +3,5 @@
 </&>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/BusinessHours b/html/Admin/Queues/Elements/Filter/BusinessHours
index a804265..ae9a3dd 100644
--- a/html/Admin/Queues/Elements/Filter/BusinessHours
+++ b/html/Admin/Queues/Elements/Filter/BusinessHours
@@ -26,4 +26,5 @@ $user_cfs->ApplySortOrder;
 </%INIT>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/ExcludedDates b/html/Admin/Queues/Elements/Filter/ExcludedDates
index 05b55d9..e73e043 100644
--- a/html/Admin/Queues/Elements/Filter/ExcludedDates
+++ b/html/Admin/Queues/Elements/Filter/ExcludedDates
@@ -37,4 +37,5 @@ $user_cfs->ApplySortOrder;
 </%INIT>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfGroup b/html/Admin/Queues/Elements/Filter/MemberOfGroup
index 3e3175f..1398bf1 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfGroup
+++ b/html/Admin/Queues/Elements/Filter/MemberOfGroup
@@ -25,4 +25,5 @@ $groups->LimitToUserDefinedGroups;
 </%INIT>
 <%ARGS>
 $prefix
+$config
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfRole b/html/Admin/Queues/Elements/Filter/MemberOfRole
index 500646a..f604ce4 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfRole
+++ b/html/Admin/Queues/Elements/Filter/MemberOfRole
@@ -30,4 +30,5 @@ if ( RT::Handle::cmp_version($RT::VERSION,'4.4.0') >= 0 ) {
 </%INIT>
 <%ARGS>
 $prefix
+$config
 </%ARGS>

commit 00d1435f28e320753c824e5fe82e166f120667f0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Aug 10 21:33:07 2016 +0000

    Rename BusinessHours to WorkSchedule

diff --git a/html/Admin/Queues/AutomaticAssignment.html b/html/Admin/Queues/AutomaticAssignment.html
index ebd6825..a5a3e06 100644
--- a/html/Admin/Queues/AutomaticAssignment.html
+++ b/html/Admin/Queues/AutomaticAssignment.html
@@ -10,7 +10,7 @@
 % my $i = 0;
 % for my $filter (@{ $config->{filters} }) {
 %    ++$i;
-%    my $name = "BusinessHours";
+%    my $name = "WorkSchedule";
 %    my $path = "/Admin/Queues/Elements/Filter/$name";
 %    my $prefix = "Filter_${name}_$i";
 
@@ -19,7 +19,7 @@
 
 <select>
 <option value="">-</option>
-<option value="BusinessHours">Business Hours</option>
+<option value="WorkSchedule">Work Schedule</option>
 <option value="ExcludedDates">Excluded Dates</option>
 <option value="MemberOfGroup">Member of Group</option>
 <option value="MemberOfRole">Member of Role</option>
diff --git a/html/Admin/Queues/Elements/Filter/BusinessHours b/html/Admin/Queues/Elements/Filter/WorkSchedule
similarity index 93%
rename from html/Admin/Queues/Elements/Filter/BusinessHours
rename to html/Admin/Queues/Elements/Filter/WorkSchedule
index ae9a3dd..bf6abdc 100644
--- a/html/Admin/Queues/Elements/Filter/BusinessHours
+++ b/html/Admin/Queues/Elements/Filter/WorkSchedule
@@ -1,4 +1,4 @@
-<&| /Widgets/TitleBox, title => loc('Business Hours') &>
+<&| /Widgets/TitleBox, title => loc('Work Schedule') &>
 <p>This filter selects eligible owners by their work schedule. The following user custom field decides which work schedule each user maintains. Administrators may update the <tt>%ServiceBusinessHours</tt> RT config for adding or modifying the available schedules.</p>
 
 % if ($user_cfs->Count) {
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter/BusinessHours.pm b/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
similarity index 90%
rename from lib/RT/Extension/AutomaticAssignment/Filter/BusinessHours.pm
rename to lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
index c137e7a..aa4d5cc 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter/BusinessHours.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
@@ -1,4 +1,4 @@
-package RT::Extension::AutomaticAssignment::Filter::BusinessHours;
+package RT::Extension::AutomaticAssignment::Filter::WorkSchedule;
 use strict;
 use warnings;
 use base 'RT::Extension::AutomaticAssignment::Filter';
@@ -70,7 +70,7 @@ sub FilterOwnersForTicket {
         for my $user (@$users) {
             my $schedule = $user->FirstCustomFieldValue($config->{user_cf});
             if (!$schedule) {
-                RT->Logger->debug("No value for user CF '$config->{user_cf}' for user " . $user->Name . "; skipping from BusinessHours automatic assignment");
+                RT->Logger->debug("No value for user CF '$config->{user_cf}' for user " . $user->Name . "; skipping from WorkSchedule automatic assignment");
                 next;
             }
 
@@ -87,7 +87,7 @@ sub FilterOwnersForTicket {
         return \@eligible;
     }
     else {
-        die "Unable to filter BusinessHours; no 'user_cf' provided.";
+        die "Unable to filter WorkSchedule; no 'user_cf' provided.";
     }
 }
 

commit 285aaf75b68e8ec55a04814acfd976bce737ab19
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 11 00:16:16 2016 +0000

    WIP

diff --git a/etc/AutomaticAssignment_Config.pm b/etc/AutomaticAssignment_Config.pm
new file mode 100644
index 0000000..484aef6
--- /dev/null
+++ b/etc/AutomaticAssignment_Config.pm
@@ -0,0 +1,16 @@
+Set($AutomaticAssignmentFilters, [qw(
+    ExcludedDates
+    MemberOfGroup
+    MemberOfRole
+    WorkSchedule
+)]) unless $AutomaticAssignmentFilters;
+
+Set($AutomaticAssignmentChoosers, [qw(
+    Random
+    RoundRobin
+    TicketStatus
+    TimeLeft
+)]) unless $AutomaticAssignmentChoosers;
+
+1;
+
diff --git a/html/Admin/Queues/AutomaticAssignment.html b/html/Admin/Queues/AutomaticAssignment.html
index a5a3e06..f780d21 100644
--- a/html/Admin/Queues/AutomaticAssignment.html
+++ b/html/Admin/Queues/AutomaticAssignment.html
@@ -2,33 +2,81 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" class="automatic-assignment" action="AutomaticAssignment.html">
+<form method="post" id="automatic-assignment" class="automatic-assignment" action="AutomaticAssignment.html">
 <input type="hidden" class="hidden" name="id" value="<%$QueueObj->Id%>" />
 
+<div class="filters">
+
 <h1>Filters</h1>
 
+<p><i>Filters reduce the pool of eligible owners. Each user must fulfill the requirements of all the filters below to be included in this queue's automatic assignment.</i></p>
+
+<select name="FilterType">
+<option value="">-</option>
+
+% for my $filter (RT->Config->Get('AutomaticAssignmentFilters')) {
+% my $class = "RT::Extension::AutomaticAssignment::Filter::$filter";
+% unless ($class->require) {
+%     RT->Logger->error("Couldn't load class '$class': $@");
+%     $m->abort;
+% }
+<option value="<% $filter %>"><% $class->Description %></option>
+% }
+
+</select>
+
+<input type="submit" class="button" name="AddFilter" value="Add Filter">
+
 % my $i = 0;
+% my $filters_value = "";
 % for my $filter (@{ $config->{filters} }) {
 %    ++$i;
-%    my $name = "WorkSchedule";
+%    my $name = $filter->{_name};
 %    my $path = "/Admin/Queues/Elements/Filter/$name";
 %    my $prefix = "Filter_${name}_$i";
+%    $filters_value .= "$prefix,";
 
-%    $m->comp($path, prefix => $prefix, config => $filter);
+%    $m->comp($path, prefix => $prefix, config => $filter, queue => $QueueObj);
 % }
 
-<select>
-<option value="">-</option>
-<option value="WorkSchedule">Work Schedule</option>
-<option value="ExcludedDates">Excluded Dates</option>
-<option value="MemberOfGroup">Member of Group</option>
-<option value="MemberOfRole">Member of Role</option>
+<input type="hidden" class="hidden" name="Filters" value="<% $filters_value %>" />
 
-</select>
-<input type="submit" class="button" value="Add Filter">
+</div>
+
+<hr />
+
+<div class="chooser">
 
 <h1>Chooser</h1>
 
+<p><i>A chooser selects a single owner from the filtered set of eligible users.</i></p>
+
+% my $chooser_config = $config->{chooser};
+% my $name = $chooser_config->{_name};
+% my $prefix = "Chooser_${name}";
+
+<select name="ChooserType">
+
+% for my $chooser (RT->Config->Get('AutomaticAssignmentChoosers')) {
+% my $class = "RT::Extension::AutomaticAssignment::Chooser::$chooser";
+% unless ($class->require) {
+%     RT->Logger->error("Couldn't load class '$class': $@");
+%     $m->abort;
+% }
+<option <% $name eq $chooser ? "selected" : "" %> value="<% $chooser %>"><% $class->Description %></option>
+% }
+
+</select>
+
+% my $path = "/Admin/Queues/Elements/Chooser/$name";
+% $m->comp($path, prefix => $prefix, config => $chooser_config, queue => $QueueObj);
+
+<input type="hidden" class="hidden" name="Chooser" value="<% $prefix %>" />
+
+</div>
+
+<& /Elements/Submit, Name => 'Update', Label => loc('Save Changes') &>
+
 </form>
 
 <%INIT>
@@ -37,11 +85,71 @@ my @results;
 my $QueueObj = RT::Queue->new($session{'CurrentUser'});
 $QueueObj->Load($id) || Abort(loc("Couldn't load queue", $id));
 
-my $attr = $QueueObj->FirstAttribute('AutomaticAssignment');
-my $config = $attr ? $attr->Content : {};
-
 my $title = loc('Automatic Assignment for queue [_1]', $QueueObj->Name);
+
+if ($Update) {
+    my %queue_config;
+
+    for my $filter_prefix (split /,/, $Filters) {
+        my @config_keys = grep { s/^\Q$filter_prefix\E_// ? $_ : () } keys %ARGS;
+        my %args = map { $_ => $ARGS{"${filter_prefix}_$_"} } @config_keys;
+        my $name = delete $args{ClassName};
+
+        next unless grep { $_ eq $name } RT->Config->Get('AutomaticAssignmentFilters');
+
+        my $class = "RT::Extension::AutomaticAssignment::Filter::$name";
+        unless ($class->require) {
+            RT->Logger->error("Couldn't load class '$class': $@");
+            $m->abort;
+        }
+
+        my $config = $class->CanonicalizeConfig(\%args);
+        $config->{_name} = $name;
+
+        push @{ $queue_config{filters} }, $config;
+    }
+
+    {
+        my @config_keys = grep { s/^\Q$Chooser\E_// ? $_ : () } keys %ARGS;
+        my %args = map { $_ => $ARGS{"${Chooser}_$_"} } @config_keys;
+        my $name = delete $args{ClassName};
+
+        next unless grep { $_ eq $name } RT->Config->Get('AutomaticAssignmentChoosers');
+
+        my $class = "RT::Extension::AutomaticAssignment::Chooser::$name";
+        unless ($class->require) {
+            RT->Logger->error("Couldn't load class '$class': $@");
+            $m->abort;
+        }
+
+        my $config = $class->CanonicalizeConfig(\%args);
+        $config->{_name} = $name;
+
+        $queue_config{chooser} = $config;
+    }
+
+    my ($ok, $msg) = $QueueObj->SetAttribute(
+        Name    => 'AutomaticAssignment',
+        Content => \%queue_config,
+    );
+
+    if ($ok) {
+        push @results, 'Automatic assignment updated';
+    }
+    else {
+        push @results, $msg;
+    }
+}
+
+my $attr = $QueueObj->FirstAttribute('AutomaticAssignment');
+my $config = $attr ? $attr->Content : {
+    filters => [],
+    chooser => { _name => 'Random' },
+};
 </%INIT>
 <%ARGS>
 $id => undef
+$Update => undef
+$Filters => undef
+$Chooser => undef
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/Random b/html/Admin/Queues/Elements/Chooser/Random
index d9f6939..036f066 100644
--- a/html/Admin/Queues/Elements/Chooser/Random
+++ b/html/Admin/Queues/Elements/Chooser/Random
@@ -1,7 +1,8 @@
-<&| /Widgets/TitleBox, title => loc('Random') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 0, class_name => 'Random' &>
 <p>This chooser selects a random owner from the pool of eligible users.</p>
 </&>
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/RoundRobin b/html/Admin/Queues/Elements/Chooser/RoundRobin
index 320633f..ab0cd8e 100644
--- a/html/Admin/Queues/Elements/Chooser/RoundRobin
+++ b/html/Admin/Queues/Elements/Chooser/RoundRobin
@@ -1,7 +1,8 @@
-<&| /Widgets/TitleBox, title => loc('Round Robin') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 0, class_name => 'RoundRobin' &>
 <p>This chooser selects the user for whom it has been the longest amount of timesince they were last automatically assigned a ticket in the same queue. This effectively distributes the tickets evenly between the userbase in a round robin fashion, even if users enter and leave the pool.</p>
 </&>
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TicketStatus b/html/Admin/Queues/Elements/Chooser/TicketStatus
index ff24381..2c9fe4a 100644
--- a/html/Admin/Queues/Elements/Chooser/TicketStatus
+++ b/html/Admin/Queues/Elements/Chooser/TicketStatus
@@ -1,7 +1,8 @@
-<&| /Widgets/TitleBox, title => loc('Ticket Status') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 0, class_name => 'TicketStatus' &>
 <p>This chooser selects the user with the fewest number of active tickets. If multiple users have the same number of tickets, then the ticket is assigned randomly to one of them.</p>
 </&>
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Chooser/TimeLeft b/html/Admin/Queues/Elements/Chooser/TimeLeft
index 2536285..32db7fb 100644
--- a/html/Admin/Queues/Elements/Chooser/TimeLeft
+++ b/html/Admin/Queues/Elements/Chooser/TimeLeft
@@ -1,7 +1,8 @@
-<&| /Widgets/TitleBox, title => loc('Time Left') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 0, class_name => 'TimeLeft' &>
 <p>This chooser selects the user with the least total amount of Time Left on their tickets in the given queue. If a particular ticket does not have Time Left, then that ticket's Time Estimated minus its Time Worked is used instead.</p> 
 </&>
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/ExcludedDates b/html/Admin/Queues/Elements/Filter/ExcludedDates
index e73e043..1d0335b 100644
--- a/html/Admin/Queues/Elements/Filter/ExcludedDates
+++ b/html/Admin/Queues/Elements/Filter/ExcludedDates
@@ -1,4 +1,4 @@
-<&| /Widgets/TitleBox, title => loc('Excluded Dates') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 1, class_name => 'ExcludedDates' &>
 <p>This filter selects eligible owners by datetime custom fields on users, meant for scheduled vacations. If the current date and time falls between the following two custom fields, the user will be excluded from automatic assignment.</p>
 
 % if ($user_cfs->Count) {
@@ -9,7 +9,7 @@
 <select id="<% $prefix %>_begin" name="<% $prefix %>_begin">
 <option value="">-</option>
 % while (my $cf = $user_cfs->Next) {
-<option value="<% $cf->Id %>"><% $cf->Name %></option>
+<option <% ($config->{begin}||0) == $cf->Id ? "selected" : "" %> value="<% $cf->Id %>"><% $cf->Name %></option>
 % }
 </select>
 </td>
@@ -20,7 +20,7 @@
 <select id="<% $prefix %>_end" name="<% $prefix %>_end">
 <option value="">-</option>
 % while (my $cf = $user_cfs->Next) {
-<option value="<% $cf->Id %>"><% $cf->Name %></option>
+<option <% ($config->{end}||0) == $cf->Id ? "selected" : "" %> value="<% $cf->Id %>"><% $cf->Name %></option>
 % }
 </select>
 </td>
@@ -38,4 +38,5 @@ $user_cfs->ApplySortOrder;
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfGroup b/html/Admin/Queues/Elements/Filter/MemberOfGroup
index 1398bf1..5dc1e4f 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfGroup
+++ b/html/Admin/Queues/Elements/Filter/MemberOfGroup
@@ -1,4 +1,4 @@
-<&| /Widgets/TitleBox, title => loc('Member of Group') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 1, class_name => 'MemberOfGroup', &>
 <p>This filter selects eligible owners by their group membership. Only members of the following group will be automatically assigned tickets.</p>
 
 % if ($groups->Count) {
@@ -9,7 +9,7 @@
 <select id="<% $prefix %>_group" name="<% $prefix %>_group">
 <option value="">-</option>
 % while (my $group = $groups->Next) {
-<option value="<% $group->Id %>"><% $group->Name %></option>
+<option <% ($config->{group}||0) == $group->Id ? "selected" : "" %> value="<% $group->Id %>"><% $group->Name %></option>
 % }
 </select>
 </td>
@@ -26,4 +26,5 @@ $groups->LimitToUserDefinedGroups;
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/MemberOfRole b/html/Admin/Queues/Elements/Filter/MemberOfRole
index f604ce4..add03b8 100644
--- a/html/Admin/Queues/Elements/Filter/MemberOfRole
+++ b/html/Admin/Queues/Elements/Filter/MemberOfRole
@@ -1,4 +1,4 @@
-<&| /Widgets/TitleBox, title => loc('Member of Role') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 1, class_name => 'MemberOfRole' &>
 <p>This filter selects eligible owners by their role membership. Only members of the following role, either on the queue or on the ticket itself, will be automatically assigned tickets.</p>
 
 <table>
@@ -8,11 +8,11 @@
 <select id="<% $prefix %>_role" name="<% $prefix %>_role">
 <option value="">-</option>
 % for my $role (qw/AdminCc Cc Requestor/) {
-<option value="<% $role %>"><% $role %></option>
+<option <% ($config->{role}||'') eq $role ? "selected" : "" %> value="<% $role %>"><% $role %></option>
 % }
 % if ($custom_roles) {
 % while (my $role = $custom_roles->Next) {
-<option value="<% $role->Id %>"><% $role->Name %></option>
+<option <% ($config->{role}||0) == $role->Id ? "selected" : "" %> value="<% $role->Id %>"><% $role->Name %></option>
 % }
 % }
 </select>
@@ -25,10 +25,12 @@
 my $custom_roles;
 if ( RT::Handle::cmp_version($RT::VERSION,'4.4.0') >= 0 ) {
     $custom_roles = RT::CustomRoles->new($session{CurrentUser});
-    $custom_roles->UnLimit;
+    $custom_roles->LimitToObjectId($queue->Id);
+    $custom_roles->ApplySortOrder;
 }
 </%INIT>
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/Filter/WorkSchedule b/html/Admin/Queues/Elements/Filter/WorkSchedule
index bf6abdc..9a5577c 100644
--- a/html/Admin/Queues/Elements/Filter/WorkSchedule
+++ b/html/Admin/Queues/Elements/Filter/WorkSchedule
@@ -1,4 +1,4 @@
-<&| /Widgets/TitleBox, title => loc('Work Schedule') &>
+<&| /Admin/Queues/Elements/SortableBox, prefix => $prefix, is_filter => 1, class_name => 'WorkSchedule' &>
 <p>This filter selects eligible owners by their work schedule. The following user custom field decides which work schedule each user maintains. Administrators may update the <tt>%ServiceBusinessHours</tt> RT config for adding or modifying the available schedules.</p>
 
 % if ($user_cfs->Count) {
@@ -9,7 +9,7 @@
 <select id="<% $prefix %>_user_cf" name="<% $prefix %>_user_cf">
 <option value="">-</option>
 % while (my $cf = $user_cfs->Next) {
-<option value="<% $cf->Id %>"><% $cf->Name %></option>
+<option <% ($config->{user_cf}||0) == $cf->Id ? "selected" : "" %> value="<% $cf->Id %>"><% $cf->Name %></option>
 % }
 </select>
 </td>
@@ -27,4 +27,5 @@ $user_cfs->ApplySortOrder;
 <%ARGS>
 $prefix
 $config
+$queue
 </%ARGS>
diff --git a/html/Admin/Queues/Elements/SortableBox b/html/Admin/Queues/Elements/SortableBox
new file mode 100644
index 0000000..f236351
--- /dev/null
+++ b/html/Admin/Queues/Elements/SortableBox
@@ -0,0 +1,27 @@
+<div data-prefix="<% $prefix %>" class="sortable-box">
+<input type="hidden" class="hidden" name="<% $prefix %>" value="1" />
+<input type="hidden" class="hidden" name="<% $prefix %>_ClassName" value="<% $class_name %>" />
+<h3><% $class->Description %></h3>
+<div class="inner">
+<% $m->content | n %>
+</div>
+</div>
+<%INIT>
+my $class;
+if ($is_filter) {
+    $class = "RT::Extension::AutomaticAssignment::Filter::$class_name";
+}
+else {
+    $class = "RT::Extension::AutomaticAssignment::Chooser::$class_name";
+}
+
+unless ($class->require) {
+    RT->Logger->error("Couldn't load class '$class': $@");
+    $m->abort;
+}
+</%INIT>
+<%ARGS>
+$is_filter
+$prefix
+$class_name
+</%ARGS>
diff --git a/html/Helpers/AddFilter b/html/Helpers/AddFilter
new file mode 100644
index 0000000..7834496
--- /dev/null
+++ b/html/Helpers/AddFilter
@@ -0,0 +1,16 @@
+<%ARGS>
+$Name => undef
+$i => 0
+$Queue => undef
+</%ARGS>
+<%INIT>
+$m->abort if !$Name || $Name =~ /\W/;
+
+my $QueueObj = RT::Queue->new($session{'CurrentUser'});
+$QueueObj->Load( $Queue );
+
+my $path = "/Admin/Queues/Elements/Filter/$Name";
+my $prefix = "Filter_${Name}_$i";
+</%INIT>
+% $m->comp($path, prefix => $prefix, config => {}, queue => $QueueObj);
+% $m->abort;
diff --git a/html/Helpers/SelectChooser b/html/Helpers/SelectChooser
new file mode 100644
index 0000000..e143775
--- /dev/null
+++ b/html/Helpers/SelectChooser
@@ -0,0 +1,15 @@
+<%ARGS>
+$Name => undef
+$Queue => undef
+</%ARGS>
+<%INIT>
+$m->abort if !$Name || $Name =~ /\W/;
+
+my $QueueObj = RT::Queue->new($session{'CurrentUser'});
+$QueueObj->Load( $Queue );
+
+my $path = "/Admin/Queues/Elements/Chooser/$Name";
+my $prefix = "Chooser_${Name}";
+</%INIT>
+% $m->comp($path, prefix => $prefix, config => {}, queue => $QueueObj);
+% $m->abort;
diff --git a/lib/RT/Extension/AutomaticAssignment.pm b/lib/RT/Extension/AutomaticAssignment.pm
index d4d3f63..5533271 100644
--- a/lib/RT/Extension/AutomaticAssignment.pm
+++ b/lib/RT/Extension/AutomaticAssignment.pm
@@ -5,6 +5,10 @@ use warnings;
 our $VERSION = '0.01';
 
 RT->AddStyleSheets("automatic-assignment.css");
+RT->AddJavaScript("automatic-assignment.js");
+
+$RT::Config::META{AutomaticAssignmentFilters}{Type} = 'ARRAY';
+$RT::Config::META{AutomaticAssignmentChoosers}{Type} = 'ARRAY';
 
 sub _LoadedClass {
     my $self      = shift;
diff --git a/lib/RT/Extension/AutomaticAssignment/Chooser.pm b/lib/RT/Extension/AutomaticAssignment/Chooser.pm
index e9ad025..7a8ac52 100644
--- a/lib/RT/Extension/AutomaticAssignment/Chooser.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Chooser.pm
@@ -8,5 +8,17 @@ sub ChooseOwnerForTicket {
     die "Subclass " . ref($self) . " of " . __PACKAGE__ . " does not implement required method ChooseOwnerForTicket";
 }
 
+sub Description {
+    my $class = shift;
+    return $class;
+}
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $config = shift;
+
+    return {};
+}
+
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Chooser/Random.pm b/lib/RT/Extension/AutomaticAssignment/Chooser/Random.pm
index 5f3db0c..c693201 100644
--- a/lib/RT/Extension/AutomaticAssignment/Chooser/Random.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Chooser/Random.pm
@@ -12,5 +12,7 @@ sub ChooseOwnerForTicket {
     return $users[rand @users];
 }
 
+sub Description { "Random" }
+
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Chooser/RoundRobin.pm b/lib/RT/Extension/AutomaticAssignment/Chooser/RoundRobin.pm
index 05799ae..e242195 100644
--- a/lib/RT/Extension/AutomaticAssignment/Chooser/RoundRobin.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Chooser/RoundRobin.pm
@@ -17,7 +17,7 @@ sub ChooseOwnerForTicket {
     # was the longest time ago
     my $owner = reduce {
         ($a->FirstAttribute($attr)||0) < ($b->FirstAttribute($attr)||0) ? $a : $b
-    } @$users;
+    } @users;
 
     if ($owner) {
         $owner->SetAttribute(Name => $attr, Content => time);
@@ -26,6 +26,7 @@ sub ChooseOwnerForTicket {
     return $owner;
 }
 
-1;
+sub Description { "Round Robin" }
 
+1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Chooser/TicketStatus.pm b/lib/RT/Extension/AutomaticAssignment/Chooser/TicketStatus.pm
index 1960b67..c13c5eb 100644
--- a/lib/RT/Extension/AutomaticAssignment/Chooser/TicketStatus.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Chooser/TicketStatus.pm
@@ -111,5 +111,7 @@ sub ChooseOwnerForTicket {
     return $fewest[rand @fewest];
 }
 
+sub Description { "Ticket Status" }
+
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Chooser/TimeLeft.pm b/lib/RT/Extension/AutomaticAssignment/Chooser/TimeLeft.pm
index 27ce449..67f463f 100644
--- a/lib/RT/Extension/AutomaticAssignment/Chooser/TimeLeft.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Chooser/TimeLeft.pm
@@ -27,6 +27,7 @@ sub ChooseOwnerForTicket {
     return reduce { $timeleft_by_owner{$a->id} < $timeleft_by_owner{$b->id} ? $a : $b } @users;
 }
 
-1;
+sub Description { "Time Left" }
 
+1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter.pm b/lib/RT/Extension/AutomaticAssignment/Filter.pm
index 88d9bb5..7ce126e 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter.pm
@@ -12,6 +12,17 @@ sub FiltersUsersArray {
     return 0;
 }
 
-1;
+sub Description {
+    my $class = shift;
+    return $class;
+}
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $config = shift;
 
+    return {};
+}
+
+1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter/ExcludedDates.pm b/lib/RT/Extension/AutomaticAssignment/Filter/ExcludedDates.pm
index 2281002..f87a7a5 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter/ExcludedDates.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter/ExcludedDates.pm
@@ -5,15 +5,15 @@ use base 'RT::Extension::AutomaticAssignment::Filter';
 
 sub _UserCF {
     my $class = shift;
-    my $name = shift;
+    my $id    = shift;
 
     my $cf = RT::CustomField->new(RT->SystemUser);
-    $cf->LoadByName(
-        Name       => $name,
+    $cf->LoadByCols(
+        id         => $id,
         LookupType => RT::User->CustomFieldLookupType,
     );
     if (!$cf->Id) {
-        die "Unable to load User Custom Field named '$name'";
+        die "Unable to load User Custom Field '$id'";
     }
     return $cf;
 }
@@ -27,17 +27,16 @@ sub FilterOwnersForTicket {
     my $now = RT::Date->new(RT->SystemUser);
     $now->SetToNow;
 
-    if ($config->{except_between}) {
-        my ($start_name, $end_name) = @{ $config->{except_between} };
-        my $start_cf = $class->_UserCF($start_name);
-        my $end_cf = $class->_UserCF($end_name);
+    if ($config->{begin} && $config->{end}) {
+        my $begin_cf = $class->_UserCF($config->{begin});
+        my $end_cf = $class->_UserCF($config->{end});
 
-        my $subclause = $start_name . '-' . $end_name;
+        my $subclause = $begin_cf->Id . '-' . $end_cf->Id;
 
-        # allow users for whom start/end is null
+        # allow users for whom begin/end is null
         $users->LimitCustomField(
             SUBCLAUSE       => "$subclause-begin",
-            CUSTOMFIELD     => $start_cf->Id,
+            CUSTOMFIELD     => $begin_cf->Id,
             COLUMN          => 'Content',
             OPERATOR        => 'IS',
             VALUE           => 'NULL',
@@ -50,12 +49,12 @@ sub FilterOwnersForTicket {
             VALUE           => 'NULL',
         );
 
-        # otherwise, "now" has to be less than start, or greater than end
-        # (expressed in the query the opposite way: start has to be greater
+        # otherwise, "now" has to be less than begin, or greater than end
+        # (expressed in the query the opposite way: begin has to be greater
         # than now or end has to be less than now)
         $users->LimitCustomField(
             SUBCLAUSE       => "$subclause-begin",
-            CUSTOMFIELD     => $start_cf->Id,
+            CUSTOMFIELD     => $begin_cf->Id,
             COLUMN          => 'Content',
             OPERATOR        => '>',
             VALUE           => $now->ISO,
@@ -76,5 +75,20 @@ sub FilterOwnersForTicket {
     }
 }
 
+sub Description { "Excluded Dates" }
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $input = shift;
+
+    my $begin = $input->{begin} || 0;
+    $begin =~ s/[^0-9]//g; # allow only numeric id
+
+    my $end = $input->{end} || 0;
+    $end =~ s/[^0-9]//g; # allow only numeric id
+
+    return { begin => $begin, end => $end };
+}
+
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfGroup.pm b/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfGroup.pm
index 05a9c49..daa0b6a 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfGroup.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfGroup.pm
@@ -9,27 +9,26 @@ sub FilterOwnersForTicket {
     my $users  = shift;
     my $config = shift;
 
-    my $group_name;
-
-    if ($config->{name}) {
-        $group_name = $config->{name};
-    }
-    elsif ($config->{queue_cf}) {
-        $group_name = $ticket->QueueObj->FirstCustomFieldValue($config->{queue_cf});
-    }
-    else {
-        die "Unable to filter MemberOfGroup; no name or queue_cf provided.";
-    }
-
     my $group = RT::Group->new($ticket->CurrentUser);
-    $group->LoadUserDefinedGroup($group_name);
+    $group->LoadUserDefinedGroup($config->{group});
 
     if (!$group->Id) {
-        die "Unable to filter MemberOfGroup; can't load group '$group_name'";
+        die "Unable to filter MemberOfGroup; can't load group '$config->{group}'";
     }
 
     $users->MemberOfGroup($group->Id);
 }
 
+sub Description { "Member of Group" }
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $input = shift;
+
+    my $group = $input->{group};
+    $group =~ s/[^0-9]//g; # allow only numeric id
+
+    return { group => $group };
+}
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfRole.pm b/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfRole.pm
index add30bc..7c465eb 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfRole.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter/MemberOfRole.pm
@@ -40,5 +40,19 @@ sub FilterOwnersForTicket {
     );
 }
 
+sub Description { "Member of Role" }
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $input = shift;
+
+    my $role = $input->{role};
+    unless ($role eq 'Cc' || $role eq 'AdminCc' || $role eq 'Requestor') {
+        $role =~ s/[^0-9]//g; # allow only numeric id
+    }
+
+    return { role => $role };
+}
+
 1;
 
diff --git a/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm b/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
index aa4d5cc..0a6a2a4 100644
--- a/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
+++ b/lib/RT/Extension/AutomaticAssignment/Filter/WorkSchedule.pm
@@ -6,15 +6,15 @@ use Business::Hours;
 
 sub _UserCF {
     my $class = shift;
-    my $name = shift;
+    my $id    = shift;
 
     my $cf = RT::CustomField->new(RT->SystemUser);
-    $cf->LoadByName(
-        Name       => $name,
+    $cf->LoadByCols(
+        id         => $id,
         LookupType => RT::User->CustomFieldLookupType,
     );
     if (!$cf->Id) {
-        die "Unable to load User Custom Field named '$name'";
+        die "Unable to load User Custom Field '$id'";
     }
     return $cf;
 }
@@ -91,5 +91,17 @@ sub FilterOwnersForTicket {
     }
 }
 
+sub Description { "Work Schedule" }
+
+sub CanonicalizeConfig {
+    my $class = shift;
+    my $input = shift;
+
+    my $cf = $input->{user_cf} || 0;
+    $cf =~ s/[^0-9]//g; # allow only numeric id
+
+    return { user_cf => $cf };
+}
+
 1;
 
diff --git a/static/css/automatic-assignment.css b/static/css/automatic-assignment.css
index 5e024a8..927cec8 100644
--- a/static/css/automatic-assignment.css
+++ b/static/css/automatic-assignment.css
@@ -1,3 +1,25 @@
 form.automatic-assignment {
     max-width: 700px;
 }
+
+.sortable-box {
+    border-style: solid;
+    border-width: 1.5px;
+    margin: 5px;
+    border-color: #aaa;
+    background-color: #eee;
+}
+
+.sortable-box .inner {
+    padding: 5px;
+}
+
+.sortable-box h3 {
+    border-bottom: 1px solid #ccc;
+    margin: 0;
+    padding: 5px 0 0 5px;
+}
+
+.sortable-box select {
+    min-width: 10em;
+}
diff --git a/static/js/automatic-assignment.js b/static/js/automatic-assignment.js
new file mode 100644
index 0000000..5701bcb
--- /dev/null
+++ b/static/js/automatic-assignment.js
@@ -0,0 +1,66 @@
+jQuery(function () {
+    var form = jQuery("#automatic-assignment");
+    var addFilterSelect = form.find('select[name=FilterType]');
+    var filtersField = form.find('input[name=Filters]');
+    var chooserField = form.find('input[name=Chooser]');
+
+    var i = form.find('.filters .sortable-box').length;
+
+    var refreshFiltersField = function () {
+        var filters = "";
+        form.find('.filters .sortable-box').each(function () {
+            filters += jQuery(this).data('prefix') + ',';
+        });
+
+        filtersField.val(filters);
+    };
+
+    form.find('input.button[name=AddFilter]').click(function (e) {
+        e.preventDefault();
+        var filter = addFilterSelect.val();
+        if (filter) {
+            var params = {
+                Name: filter,
+                i: ++i
+            };
+
+            jQuery.ajax({
+                url: RT.Config.WebHomePath + "/Helpers/AddFilter",
+                data: params,
+                success: function (html) {
+                    form.find('.filters').append(html);
+                    refreshFiltersField();
+                    addFilterSelect.val('');
+                },
+                error: function (xhr, reason) {
+                    alert(reason);
+                }
+            });
+        }
+        else {
+            alert("Please select a filter.");
+        }
+    });
+
+    form.find('select[name=ChooserType]').change(function (e) {
+        e.preventDefault();
+        var chooser = jQuery(this).val();
+        var params = {
+            Name: chooser
+        };
+        jQuery('.chooser .sortable-box').empty();
+
+        jQuery.ajax({
+            url: RT.Config.WebHomePath + "/Helpers/SelectChooser",
+            data: params,
+            success: function (html) {
+                form.find('.chooser .sortable-box').replaceWith(html);
+                chooserField.val('Chooser_' + chooser);
+            },
+            error: function (xhr, reason) {
+                alert(reason);
+            }
+        });
+    });
+});
+

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


More information about the Bps-public-commit mailing list