Compare commits
10 commits
daeabc1aa6
...
8216ff4071
Author | SHA1 | Date | |
---|---|---|---|
|
8216ff4071 | ||
|
8514ae62d5 | ||
|
727d9d5659 | ||
|
325c5ceaf6 | ||
|
43e51ac66c | ||
|
3f58ff316d | ||
|
83243abf2c | ||
|
50db9059bd | ||
|
50e684e755 | ||
|
d7a24b0f97 |
12 changed files with 311 additions and 25 deletions
7
etc/OneTicketPerRequestor.yml
Normal file
7
etc/OneTicketPerRequestor.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
# Keys are queue names.
|
||||||
|
# Values are either a string, or an array of strings, with
|
||||||
|
# TicketSQL ordering like "Created DESC" or "id ASC".
|
||||||
|
# For each queue in this file, the OneTicketPerRequestor mail plug-in
|
||||||
|
# will redirect incoming mail to the first ticket from the requestor
|
||||||
|
# that matches with the ordering you specified.
|
8
etc/upgrade/0.2/content
Normal file
8
etc/upgrade/0.2/content
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
@ScripActions = (
|
||||||
|
{ Name => 'Notify Owner or Queue AdminCcs',
|
||||||
|
Description => 'Sends mail to ticket AdminCCs, plus the Owner if set, otherwise queue AdminCCs',
|
||||||
|
ExecModule => 'NotifyOwnerOrQueueAdminCcs',
|
||||||
|
},
|
||||||
|
);
|
28
etc/upgrade/0.3/content
Normal file
28
etc/upgrade/0.3/content
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
@ScripConditions = (
|
||||||
|
{ Name => 'On Close With Dependents',
|
||||||
|
Description => 'Whenever a ticket is closed and other tickets depend on it',
|
||||||
|
ApplicableTransTypes => 'Status',
|
||||||
|
ExecModule => 'StatusChangeWithDependents',
|
||||||
|
Argument => 'dependents > 0; old: initial, active; new: inactive',
|
||||||
|
},
|
||||||
|
{ Name => 'On Close Without Dependents',
|
||||||
|
Description => 'Whenever a ticket is closed and no other tickets depend on it',
|
||||||
|
ApplicableTransTypes => 'Status',
|
||||||
|
ExecModule => 'StatusChangeWithDependents',
|
||||||
|
Argument => 'dependents == 0; old: initial, active; new: inactive',
|
||||||
|
},
|
||||||
|
{ Name => 'On Approved With Dependents',
|
||||||
|
Description => 'Whenever a ticket is an approval and other tickets depend on it',
|
||||||
|
ApplicableTransTypes => 'Status',
|
||||||
|
ExecModule => 'StatusChangeWithDependents',
|
||||||
|
Argument => 'dependents > 0; new: approved',
|
||||||
|
},
|
||||||
|
{ Name => 'On Approved Without Dependents',
|
||||||
|
Description => 'Whenever a ticket is an approval and no other tickets depend on it',
|
||||||
|
ApplicableTransTypes => 'Status',
|
||||||
|
ExecModule => 'StatusChangeWithDependents',
|
||||||
|
Argument => 'dependents == 0; new: approved',
|
||||||
|
},
|
||||||
|
);
|
12
etc/upgrade/0.4/content
Normal file
12
etc/upgrade/0.4/content
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
@ScripActions = (
|
||||||
|
{ Name => 'Add a Comment',
|
||||||
|
Description => 'Add a comment to the ticket',
|
||||||
|
ExecModule => 'RecordComment',
|
||||||
|
},
|
||||||
|
{ Name => 'Add Correspondence',
|
||||||
|
Description => 'Add correspondence to the ticket',
|
||||||
|
ExecModule => 'RecordCorrespondence',
|
||||||
|
},
|
||||||
|
);
|
8
etc/upgrade/0.5/content
Normal file
8
etc/upgrade/0.5/content
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
@ScripActions = (
|
||||||
|
{ Name => 'Notify Owner or Queue AdminCcs as Comment',
|
||||||
|
Description => 'Sends comment mail to ticket AdminCCs, plus the Owner if set, otherwise queue AdminCCs',
|
||||||
|
ExecModule => 'NotifyOwnerOrQueueAdminCcsAsComment',
|
||||||
|
},
|
||||||
|
);
|
28
etc/upgrade/0.6/content
Normal file
28
etc/upgrade/0.6/content
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
@ScripConditions = (
|
||||||
|
{ Name => 'On Create Inbound',
|
||||||
|
Description => 'When a ticket is created by its requestor',
|
||||||
|
ApplicableTransTypes => 'Create',
|
||||||
|
ExecModule => 'CheckTransactionFlags',
|
||||||
|
Argument => 'IsInbound',
|
||||||
|
},
|
||||||
|
{ Name => 'On Create Not Inbound',
|
||||||
|
Description => 'When a ticket is created for the requestor by someone else',
|
||||||
|
ApplicableTransTypes => 'Create',
|
||||||
|
ExecModule => 'CheckTransactionFlags',
|
||||||
|
Argument => '!IsInbound',
|
||||||
|
},
|
||||||
|
{ Name => 'On Create Not Inbound With Content',
|
||||||
|
Description => 'When a ticket with content is created for the requestor by someone else',
|
||||||
|
ApplicableTransTypes => 'Create',
|
||||||
|
ExecModule => 'CheckTransactionFlags',
|
||||||
|
Argument => '!IsInbound HasContent',
|
||||||
|
},
|
||||||
|
{ Name => 'On Create Not Inbound Without Content',
|
||||||
|
Description => 'When an empty ticket is created for the requestor by someone else',
|
||||||
|
ApplicableTransTypes => 'Create',
|
||||||
|
ExecModule => 'CheckTransactionFlags',
|
||||||
|
Argument => '!IsInbound !HasContent',
|
||||||
|
},
|
||||||
|
);
|
23
lib/RT/Action/NotifyOwnerOrQueueAdminCcs.pm
Normal file
23
lib/RT/Action/NotifyOwnerOrQueueAdminCcs.pm
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package RT::Action::NotifyOwnerOrQueueAdminCcs;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base qw(RT::Action::NotifyOwnerOrAdminCc);
|
||||||
|
|
||||||
|
sub SetRecipients {
|
||||||
|
my $self = shift;
|
||||||
|
$self->SUPER::SetRecipients();
|
||||||
|
my $ticket = $self->TicketObj;
|
||||||
|
# If the ticket has an owner, that person was set as the recipient.
|
||||||
|
# Now add ticket AdminCcs to the Bcc list.
|
||||||
|
if ($ticket->Owner != RT->Nobody->id) {
|
||||||
|
push(@{$self->{Bcc}}, $ticket->AdminCc->MemberEmailAddresses);
|
||||||
|
}
|
||||||
|
# If the ticket has no owner, both queue and ticket AdminCCs were set as
|
||||||
|
# recipients. There's nothing else to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
RT::Base->_ImportOverlays();
|
||||||
|
|
||||||
|
1;
|
15
lib/RT/Action/NotifyOwnerOrQueueAdminCcsAsComment.pm
Normal file
15
lib/RT/Action/NotifyOwnerOrQueueAdminCcsAsComment.pm
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package RT::Action::NotifyOwnerOrQueueAdminCcsAsComment;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use RT::Action::NotifyAsComment;
|
||||||
|
|
||||||
|
use mro "c3";
|
||||||
|
use base qw(
|
||||||
|
RT::Action::NotifyOwnerOrQueueAdminCcs
|
||||||
|
RT::Action::NotifyAsComment
|
||||||
|
);
|
||||||
|
|
||||||
|
RT::Base->_ImportOverlays();
|
||||||
|
|
||||||
|
1;
|
29
lib/RT/Condition/CheckTransactionFlags.pm
Normal file
29
lib/RT/Condition/CheckTransactionFlags.pm
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package RT::Condition::CheckTransactionFlags;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base qw(RT::Condition);
|
||||||
|
|
||||||
|
sub IsApplicable {
|
||||||
|
my $self = shift;
|
||||||
|
my $txn = $self->TransactionObj;
|
||||||
|
my @flags = split(" ", $self->Argument || "");
|
||||||
|
foreach my $flag (@flags) {
|
||||||
|
unless ($flag =~ /^(!?)(\w+)$/) {
|
||||||
|
$RT::Logger->Error("Argument '$flag' is malformed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
my $method = $txn->can($2);
|
||||||
|
unless (defined($method)) {
|
||||||
|
$RT::Logger->Error("Argument '$flag' refers to nonexistent transaction method");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0 unless (!!$txn->$method == !$1);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RT::Base->_ImportOverlays();
|
||||||
|
|
||||||
|
1;
|
34
lib/RT/Condition/StatusChangeWithDependents.pm
Normal file
34
lib/RT/Condition/StatusChangeWithDependents.pm
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package RT::Condition::StatusChangeWithDependents;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base qw(RT::Condition::StatusChange);
|
||||||
|
|
||||||
|
sub Argument {
|
||||||
|
# Take the dependents setting out of the argument, so StatusChange gets
|
||||||
|
# everything else.
|
||||||
|
my $self = shift;
|
||||||
|
$self->SUPER::Argument =~ s/^\s*dependents\s*(<|>|[=!<>]=)\s*(\d+)\s*;?//r;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub IsApplicable {
|
||||||
|
my $self = shift;
|
||||||
|
my $argument = $self->SUPER::Argument;
|
||||||
|
my ($op, $operand);
|
||||||
|
if ($argument =~ /^\s*dependents\s*(<|>|[=!<>]=)\s*(\d+)\s*;?/) {
|
||||||
|
$op = $1;
|
||||||
|
$operand = $2;
|
||||||
|
} else {
|
||||||
|
$RT::Logger->error("Argument '$argument' is incorrect");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0 unless $self->SUPER::IsApplicable;
|
||||||
|
my $dep_count = $self->TicketObj->DependedOnBy->Count;
|
||||||
|
return 0 unless (eval "\$dep_count $op $operand;");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RT::Base->_ImportOverlays();
|
||||||
|
|
||||||
|
1;
|
|
@ -1,12 +1,102 @@
|
||||||
# This extension is free software.
|
|
||||||
# You may distribute it under the terms of the GNU General Public License,
|
|
||||||
# version 3.0, or (at your option) any later version published by the
|
|
||||||
# Free Software Foundation.
|
|
||||||
|
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
package RT::Extension::Conservancy;
|
package RT::Extension::Conservancy;
|
||||||
|
|
||||||
our $VERSION = '0.1';
|
our $VERSION = '0.6';
|
||||||
|
|
||||||
|
=pod
|
||||||
|
|
||||||
|
=head1 RT::Extension::Conservancy
|
||||||
|
|
||||||
|
A collection of RT extensions developed for Software Freedom Conservancy.
|
||||||
|
|
||||||
|
=head2 Contents
|
||||||
|
|
||||||
|
=head3 RT::Action::NotifyOwnerOrQueueAdminCcs
|
||||||
|
|
||||||
|
A Scrip action that sends notice to the ticket owner if that's set, or queue
|
||||||
|
AdminCCs if not. Ticket AdminCCs always get the notice too, which is how
|
||||||
|
this is different from the built-in "Notify Owner or AdminCCs".
|
||||||
|
|
||||||
|
=head3 RT::Action::NotifyOwnerOrQueueAdminCcsAsComment
|
||||||
|
|
||||||
|
A variant of the above to send notice from the comment address.
|
||||||
|
|
||||||
|
=head3 RT::Condition::CheckTransactionFlags
|
||||||
|
|
||||||
|
A Scrip condition that calls configured boolean methods on the transaction
|
||||||
|
object and passes or fails depending on whether they match the configured
|
||||||
|
state.
|
||||||
|
|
||||||
|
=head3 RT::Condition::StatusChangeWithDependents
|
||||||
|
|
||||||
|
A Scrip condition that extends RT::Condition::StatusChange. The affected
|
||||||
|
ticket must have a configured number of other tickets that Depend On it for
|
||||||
|
this condition to be true.
|
||||||
|
|
||||||
|
=head3 RT::Interface::Email::OneTicketPerRequestor
|
||||||
|
|
||||||
|
A mail plugin that examines incoming mail to see if it would create a new
|
||||||
|
ticket. If so, and the requestor already has a ticket in the destination
|
||||||
|
queue, the mail is turned into correspondence on the existing ticket
|
||||||
|
instead.
|
||||||
|
|
||||||
|
You can configure which queues this works for, and what ticket the mail gets
|
||||||
|
merged into (in case there's more than one from ticket history, or admins
|
||||||
|
creating multiple through the web interface, etc.). Edit
|
||||||
|
C<etc/OneTicketPerRequestor.yml> following the instructions in the comments.
|
||||||
|
|
||||||
|
=head2 Requirements
|
||||||
|
|
||||||
|
Works with RT 4.4.
|
||||||
|
|
||||||
|
=head2 Installation
|
||||||
|
|
||||||
|
=over 4
|
||||||
|
|
||||||
|
=item Check out this repository in your RT plugins directory.
|
||||||
|
|
||||||
|
=item Edit your RT site configuration to include the line
|
||||||
|
C<Plugin("RT-Extension-Conservancy")>.
|
||||||
|
|
||||||
|
=item If you want to use the OneTicketPerRequestor mail plugin, make sure
|
||||||
|
your site configuration sets C<@MailPlugins>, and the list includes
|
||||||
|
C<"OneTicketPerRequestor">.
|
||||||
|
|
||||||
|
=item Run in a shell from your checkout:
|
||||||
|
C<for f in etc/upgrade/*/content; do rt-setup-database --action insert --datafile "$(realpath -s "$f")" || break; done>
|
||||||
|
|
||||||
|
=item Restart your RT server.
|
||||||
|
|
||||||
|
=back
|
||||||
|
|
||||||
|
=head2 Upgrading
|
||||||
|
|
||||||
|
=over 4
|
||||||
|
|
||||||
|
=item Before you upgrade anything, note the C<$VERSION> of this extension
|
||||||
|
you're currently running.
|
||||||
|
|
||||||
|
=item Update your plugin checkout to the version you want with C<git pull>,
|
||||||
|
C<git checkout>, etc.
|
||||||
|
|
||||||
|
=item Repeat the C<rt-setup-database> step from the Installation step,
|
||||||
|
running it only on the versions that are later than the C<$VERSION> you
|
||||||
|
noted two steps ago.
|
||||||
|
|
||||||
|
=item Restart your RT server.
|
||||||
|
|
||||||
|
=back
|
||||||
|
|
||||||
|
=head2 Copyright and License
|
||||||
|
|
||||||
|
Copyright © 2018 Brett Smith <brettcsmith@brettcsmith.org>
|
||||||
|
|
||||||
|
This extension is free software.
|
||||||
|
You may distribute it under the terms of the GNU General Public License,
|
||||||
|
version 3.0, or (at your option) any later version published by the
|
||||||
|
Free Software Foundation.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -18,10 +18,8 @@ my $plugin_self = RT::Plugin->new(name => "RT-Extension-Conservancy");
|
||||||
foreach my $confdir ($plugin_self->Path("etc"), $RT::LocalEtcPath, $RT::EtcPath) {
|
foreach my $confdir ($plugin_self->Path("etc"), $RT::LocalEtcPath, $RT::EtcPath) {
|
||||||
next unless (defined($confdir) && $confdir);
|
next unless (defined($confdir) && $confdir);
|
||||||
my $conf_path = "$confdir/OneTicketPerRequestor.yml";
|
my $conf_path = "$confdir/OneTicketPerRequestor.yml";
|
||||||
if (-r $conf_path) {
|
eval { %QUEUES = %{YAML::Tiny->read($conf_path)->[0]} };
|
||||||
%QUEUES = %{YAML::Tiny->read($conf_path)->[0]};
|
last if (%QUEUES);
|
||||||
last if (%QUEUES);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Compile the user's configuration into arguments for RT::Tickets->OrderBy.
|
# Compile the user's configuration into arguments for RT::Tickets->OrderBy.
|
||||||
|
@ -77,22 +75,28 @@ sub BeforeDecrypt {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $ticket_search = RT::Tickets->new($RT::SystemUser);
|
my $dest_ticket = eval {
|
||||||
$ticket_search->LimitQueue(
|
my $ticket_search = RT::Tickets->new($RT::SystemUser);
|
||||||
OPERATOR => "=",
|
$ticket_search->LimitQueue(
|
||||||
VALUE => $queue_name,
|
OPERATOR => "=",
|
||||||
);
|
VALUE => $queue_name,
|
||||||
$ticket_search->LimitWatcher(
|
);
|
||||||
TYPE => "Requestor",
|
$ticket_search->LimitWatcher(
|
||||||
OPERATOR => "=",
|
TYPE => "Requestor",
|
||||||
VALUE => $from_address,
|
OPERATOR => "=",
|
||||||
);
|
VALUE => $from_address,
|
||||||
foreach my $sort_args (@$sort_orders) {
|
);
|
||||||
$ticket_search->OrderBy(@$sort_args);
|
foreach my $sort_args (@$sort_orders) {
|
||||||
|
$ticket_search->OrderBy(@$sort_args);
|
||||||
|
}
|
||||||
|
$RT::Logger->debug("OTPR search: " . $ticket_search->BuildSelectQuery());
|
||||||
|
$ticket_search->First;
|
||||||
|
};
|
||||||
|
if ($@) {
|
||||||
|
$RT::Logger->debug("OTPR stopping: Ticket search died: $@");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
elsif (!defined($dest_ticket)) {
|
||||||
my $dest_ticket = $ticket_search->First;
|
|
||||||
if (!defined($dest_ticket)) {
|
|
||||||
$RT::Logger->debug(sprintf("OTPR stopping: No ticket found from <%s> in queue %s",
|
$RT::Logger->debug(sprintf("OTPR stopping: No ticket found from <%s> in queue %s",
|
||||||
$from_address, $queue_name));
|
$from_address, $queue_name));
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Reference in a new issue