Bug 185090: Add revamped whining system

patch by: Erik
r=joel
r=jouni
a=justdave
This commit is contained in:
bugreport%peshkin.net 2004-08-04 21:36:27 +00:00
Родитель 1f7a267509
Коммит b84eb0ffec
13 изменённых файлов: 1786 добавлений и 7 удалений

Просмотреть файл

@ -172,10 +172,14 @@ sub queries {
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
my $sth = $dbh->prepare(q{ SELECT name, query, linkinfooter
FROM namedqueries
WHERE userid=?
ORDER BY UPPER(name)});
my $sth = $dbh->prepare(q{ SELECT
DISTINCT name, query, linkinfooter,
IF(whine_queries.id IS NOT NULL, 1, 0)
FROM namedqueries
LEFT JOIN whine_queries
ON whine_queries.query_name = name
WHERE userid=?
ORDER BY UPPER(name)});
$sth->execute($self->{id});
my @queries;
@ -184,6 +188,7 @@ sub queries {
name => $row->[0],
query => $row->[1],
linkinfooter => $row->[2],
usedinwhine => $row->[3],
});
}
$self->{queries} = \@queries;

Просмотреть файл

@ -1262,7 +1262,8 @@ WriteParams();
# These are the files which need to be marked executable
my @executable_files = ('whineatnews.pl', 'collectstats.pl',
'checksetup.pl', 'importxml.pl', 'runtests.sh', 'testserver.pl');
'checksetup.pl', 'importxml.pl', 'runtests.sh', 'testserver.pl',
'whine.pl');
# tell me if a file is executable. All CGI files and those in @executable_files
# are executable
@ -1989,6 +1990,37 @@ $table{series_categories} =
unique(name)';
# whine system
$table{whine_queries} =
'id mediumint auto_increment primary key,
eventid mediumint not null,
query_name varchar(64) not null default \'\',
sortkey smallint not null default 0,
onemailperbug tinyint not null default 0,
title varchar(128) not null,
index(eventid)';
$table{whine_schedules} =
'id mediumint auto_increment primary key,
eventid mediumint not null,
run_day varchar(32),
run_time varchar(32),
run_next datetime,
mailto_userid mediumint not null,
index(run_next),
index(eventid)';
$table{whine_events} =
'id mediumint auto_increment primary key,
owner_userid mediumint not null,
subject varchar(128),
body mediumtext';
###########################################################################
# Create tables
###########################################################################
@ -4012,6 +4044,18 @@ if (!GroupDoesExist("canconfirm")) {
}
# Create bz_canusewhineatothers and bz_canusewhines
if (!GroupDoesExist('bz_canusewhines')) {
my $whine_group = AddGroup('bz_canusewhines',
'User can configure whine reports for self');
my $whineatothers_group = AddGroup('bz_canusewhineatothers',
'Can configure whine reports for ' .
'other users');
$dbh->do("INSERT IGNORE INTO group_group_map " .
"(member_id, grantor_id, grant_type) " .
"VALUES (${whine_group}, ${whineatothers_group}, " .
GROUP_MEMBERSHIP . ")");
}
###########################################################################
# Create Administrator --ADMIN--

433
webtools/bugzilla/editwhines.cgi Executable file
Просмотреть файл

@ -0,0 +1,433 @@
#!/usr/bin/perl -wT
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
#
################################################################################
# Script Initialization
################################################################################
use strict;
use lib ".";
require "CGI.pl";
require "globals.pl";
use vars qw( $vars );
use Bugzilla::Constants;
use Bugzilla::User;
# require the user to have logged in
Bugzilla->login(LOGIN_REQUIRED);
###############################################################################
# Main Body Execution
###############################################################################
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my $userid = $user->id;
my $sth; # database statement handle
# $events is a hash ref, keyed by event id, that stores the active user's
# events. It starts off with:
# 'subject' - the subject line for the email message
# 'body' - the text to be sent at the top of the message
#
# Eventually, it winds up with:
# 'queries' - array ref containing hashes of:
# 'name' - the name of the saved query
# 'title' - The title line for the search results table
# 'sort' - Numeric sort ID
# 'id' - row ID for the query entry
# 'onemailperbug' - whether a single message must be sent for each
# result.
# 'schedule' - array ref containing hashes of:
# 'day' - Day or range of days this schedule will be run
# 'time' - time or interval to run
# 'mailto' - person who will receive the results
# 'id' - row ID for the schedule
my $events = get_events($userid);
# First see if this user may use whines
ThrowUserError('whine_access_denied') unless (UserInGroup('bz_canusewhines'));
# May this user send mail to other users?
my $can_mail_others = UserInGroup('bz_canusewhineatothers');
# If the form was submitted, we need to look for what needs to be added or
# removed, then what was altered.
if ($cgi->param('update')) {
if ($cgi->param("add_event")) {
# we create a new event
$sth = $dbh->prepare("INSERT INTO whine_events " .
"(owner_userid) " .
"VALUES (?)");
$sth->execute($userid);
}
else {
for my $eventid (keys %{$events}) {
# delete an entire event
if ($cgi->param("remove_event_$eventid")) {
# We need to make sure these belong to the same user,
# otherwise we could simply delete whatever matched that ID.
#
# schedules
$sth = $dbh->prepare("SELECT whine_schedules.id " .
"FROM whine_schedules " .
"LEFT JOIN whine_events " .
"ON whine_events.id = " .
"whine_schedules.eventid " .
"WHERE whine_events.id = ? " .
"AND whine_events.owner_userid = ?");
$sth->execute($eventid, $userid);
my @ids = @{$sth->fetchall_arrayref};
$sth = $dbh->prepare("DELETE FROM whine_schedules "
. "WHERE id=?");
for (@ids) {
my $delete_id = $_->[0];
$sth->execute($delete_id);
}
# queries
$sth = $dbh->prepare("SELECT whine_queries.id " .
"FROM whine_queries " .
"LEFT JOIN whine_events " .
"ON whine_events.id = " .
"whine_queries.eventid " .
"WHERE whine_events.id = ? " .
"AND whine_events.owner_userid = ?");
$sth->execute($eventid, $userid);
@ids = @{$sth->fetchall_arrayref};
$sth = $dbh->prepare("DELETE FROM whine_queries " .
"WHERE id=?");
for (@ids) {
my $delete_id = $_->[0];
$sth->execute($delete_id);
}
# events
$sth = $dbh->prepare("DELETE FROM whine_events " .
"WHERE id=? AND owner_userid=?");
$sth->execute($eventid, $userid);
}
else {
# check the subject and body for changes
my $subject = ($cgi->param("event_${eventid}_subject") or '');
my $body = ($cgi->param("event_${eventid}_body") or '');
trick_taint($subject) if $subject;
trick_taint($body) if $body;
if ( ($subject ne $events->{$eventid}->{'subject'})
|| ($body ne $events->{$eventid}->{'body'}) ) {
$sth = $dbh->prepare("UPDATE whine_events " .
"SET subject=?, body=? " .
"WHERE id=?");
$sth->execute($subject, $body, $eventid);
}
# add a schedule
if ($cgi->param("add_schedule_$eventid")) {
# the schedule table must be locked before altering
$sth = $dbh->prepare("INSERT INTO whine_schedules " .
"(eventid, mailto_userid, " .
"run_day, run_time) " .
"VALUES (?, ?, 'Sun', 2)");
$sth->execute($eventid, $userid);
}
# add a query
elsif ($cgi->param("add_query_$eventid")) {
$sth = $dbh->prepare("INSERT INTO whine_queries "
. "(eventid) "
. "VALUES (?)");
$sth->execute($eventid);
}
}
# now check all of the schedules and queries to see if they need
# to be altered or deleted
# Check schedules for changes
$sth = $dbh->prepare("SELECT id " .
"FROM whine_schedules " .
"WHERE eventid=?");
$sth->execute($eventid);
my @scheduleids = ();
for (@{$sth->fetchall_arrayref}) {
push @scheduleids, $_->[0];
};
# we need to double-check all of the user IDs in mailto to make
# sure they exist
my $arglist = {}; # args for match_field
for my $sid (@scheduleids) {
$arglist->{"mailto_$sid"} = {
'type' => 'single',
};
}
if (scalar %{$arglist}) {
&Bugzilla::User::match_field($arglist);
}
for my $sid (@scheduleids) {
if ($cgi->param("remove_schedule_$sid")) {
# having the owner id in here is a security failsafe
$sth = $dbh->prepare("SELECT whine_schedules.id " .
"FROM whine_schedules " .
"LEFT JOIN whine_events " .
"ON whine_events.id = " .
"whine_schedules.eventid " .
"WHERE whine_events.owner_userid=? " .
"AND whine_schedules.id =?");
$sth->execute($userid, $sid);
my @ids = @{$sth->fetchall_arrayref};
for (@ids) {
$sth = $dbh->prepare("DELETE FROM whine_schedules " .
"WHERE id=?");
$sth->execute($_->[0]);
}
}
else {
my $o_day = $cgi->param("orig_day_$sid");
my $day = $cgi->param("day_$sid");
my $o_time = $cgi->param("orig_time_$sid");
my $time = $cgi->param("time_$sid");
my $o_mailto = $cgi->param("orig_mailto_$sid");
my $mailto = $cgi->param("mailto_$sid");
$o_day = '' unless length($o_day);
$o_time = '' unless length($o_time);
$o_mailto = '' unless length($o_mailto);
$day = '' unless length($day);
$time = '' unless length($time);
$mailto = '' unless length($mailto);
my $mail_uid = $userid;
# get a userid for the mailto address
if ($can_mail_others and $mailto) {
trick_taint($mailto);
$mail_uid = DBname_to_id($mailto);
}
if ( ($o_day ne $day) ||
($o_time ne $time) ){
trick_taint($day) if length($day);
trick_taint($time) if length($time);
# the schedule table must be locked
$sth = $dbh->prepare("UPDATE whine_schedules " .
"SET run_day=?, run_time=?, " .
"mailto_userid=?, " .
"run_next=NULL " .
"WHERE id=?");
$sth->execute($day, $time, $mail_uid, $sid);
}
}
}
# Check queries for changes
$sth = $dbh->prepare("SELECT id " .
"FROM whine_queries " .
"WHERE eventid=?");
$sth->execute($eventid);
my @queries = ();
for (@{$sth->fetchall_arrayref}) {
push @queries, $_->[0];
};
for my $qid (@queries) {
if ($cgi->param("remove_query_$qid")) {
$sth = $dbh->prepare("SELECT whine_queries.id " .
"FROM whine_queries " .
"LEFT JOIN whine_events " .
"ON whine_events.id = " .
"whine_queries.eventid " .
"WHERE whine_events.owner_userid=? " .
"AND whine_queries.id =?");
$sth->execute($userid, $qid);
for (@{$sth->fetchall_arrayref}) {
$sth = $dbh->prepare("DELETE FROM whine_queries " .
"WHERE id=?");
$sth->execute($_->[0]);
}
}
else {
my $o_sort = $cgi->param("orig_query_sort_$qid");
my $sort = $cgi->param("query_sort_$qid");
my $o_queryname = $cgi->param("orig_query_name_$qid");
my $queryname = $cgi->param("query_name_$qid");
my $o_title = $cgi->param("orig_query_title_$qid");
my $title = $cgi->param("query_title_$qid");
my $o_onemailperbug =
$cgi->param("orig_query_onemailperbug_$qid");
my $onemailperbug =
$cgi->param("query_onemailperbug_$qid");
$o_sort = '' unless length($o_sort);
$o_queryname = '' unless length($o_queryname);
$o_title = '' unless length($o_title);
$o_onemailperbug = '' unless length($o_onemailperbug);
$sort = '' unless length($sort);
$queryname = '' unless length($queryname);
$title = '' unless length($title);
$onemailperbug = '' unless length($onemailperbug);
if ($onemailperbug eq 'on') {
$onemailperbug = 1;
}
elsif ($onemailperbug eq 'off') {
$onemailperbug = 0;
}
if ( ($o_sort ne $sort) ||
($o_queryname ne $queryname) ||
($o_onemailperbug xor $onemailperbug) ||
($o_title ne $title) ){
detaint_natural($sort) if length $sort;
trick_taint($queryname) if length $queryname;
trick_taint($title) if length $title;
trick_taint($onemailperbug) if length $onemailperbug;
$sth = $dbh->prepare("UPDATE whine_queries " .
"SET sortkey=?, " .
"query_name=?, " .
"title=?, " .
"onemailperbug=? " .
"WHERE id=?");
$sth->execute($sort, $queryname, $title,
$onemailperbug, $qid);
}
}
}
}
}
}
$vars->{'mail_others'} = $can_mail_others;
# Return the appropriate HTTP response headers.
print $cgi->header();
# Get events again, to cover any updates that were made
$events = get_events($userid);
# Here is the data layout as sent to the template:
#
# events
# event_id #
# schedule
# day
# time
# mailto
# queries
# name
# title
# sort
#
# build the whine list by event id
for my $event_id (keys %{$events}) {
$events->{$event_id}->{'schedule'} = [];
$events->{$event_id}->{'queries'} = [];
# schedules
$sth = $dbh->prepare("SELECT run_day, run_time, profiles.login_name, id " .
"FROM whine_schedules " .
"LEFT JOIN profiles " .
"ON whine_schedules.mailto_userid = " .
"profiles.userid " .
"WHERE eventid=?");
$sth->execute($event_id);
for my $row (@{$sth->fetchall_arrayref}) {
my $this_schedule = {
'day' => $row->[0],
'time' => $row->[1],
'mailto' => $row->[2],
'id' => $row->[3],
};
push @{$events->{$event_id}->{'schedule'}}, $this_schedule;
}
# queries
$sth = $dbh->prepare("SELECT query_name, title, sortkey, id, " .
"onemailperbug " .
"FROM whine_queries " .
"WHERE eventid=? " .
"ORDER BY sortkey");
$sth->execute($event_id);
for my $row (@{$sth->fetchall_arrayref}) {
my $this_query = {
'name' => $row->[0],
'title' => $row->[1],
'sort' => $row->[2],
'id' => $row->[3],
'onemailperbug' => $row->[4],
};
push @{$events->{$event_id}->{'queries'}}, $this_query;
}
}
$vars->{'events'} = $events;
# get the available queries
$sth = $dbh->prepare("SELECT name FROM namedqueries WHERE userid=?");
$sth->execute($userid);
$vars->{'available_queries'} = [];
while (my $query = $sth->fetch) {
push @{$vars->{'available_queries'}}, $query->[0];
}
$template->process("whine/schedule.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
# get_events takes a userid and returns a hash, keyed by event ID, containing
# the subject and body of each event that user owns
sub get_events {
my $userid = shift;
my $events = {};
my $sth = $dbh->prepare("SELECT DISTINCT id, subject, body " .
"FROM whine_events " .
"WHERE owner_userid=?");
$sth->execute($userid);
for (@{$sth->fetchall_arrayref}) {
$events->{$_->[0]} = {
'subject' => $_->[1],
'body' => $_->[2],
}
}
return $events;
}

Просмотреть файл

@ -70,8 +70,12 @@
<a href="query.cgi?[% q.query FILTER html %]">Edit</a>
</td>
<td>
<a href="buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
[% q.name FILTER html %]">Forget</a>
[% IF q.usedinwhine %]
Remove from <a href="editwhines.cgi">whining</a> first
[% ELSE %]
<a href="buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
[% q.name FILTER html %]">Forget</a>
[% END %]
</td>
<td align="center">
<input type="checkbox"

Просмотреть файл

@ -37,6 +37,19 @@
%::safe = (
'whine/schedule.html.tmpl' => [
'event.key',
'query.id',
'query.sort',
'schedule.id',
'option.0',
'option.1',
],
'whine/mail.html.tmpl' => [
'bug.bug_id',
],
'sidebar.xul.tmpl' => [
'template_version',
],

Просмотреть файл

@ -104,6 +104,8 @@
href="editgroups.cgi">' IF user.groups.creategroups %]
[% '<link rel="Administration" title="Keywords"
href="editkeywords.cgi">' IF user.groups.editkeywords %]
[% '<link rel="Administration" title="Whining"
href="editwhines.cgi">' IF user.groups.bz_canusewhines %]
[% '<link rel="Administration" title="Sanity Check"
href="sanitycheck.cgi">' IF user.groups.tweakparams %]
[% END %]

Просмотреть файл

@ -81,6 +81,8 @@
IF user.groups.creategroups %]
[% ' | <a href="editkeywords.cgi">Keywords</a>'
IF user.groups.editkeywords %]
[% ' | <a href="editwhines.cgi">Whining</a>'
IF user.groups.bz_canusewhines %]
</div>
</div>
[% END %]

Просмотреть файл

@ -846,6 +846,11 @@
Value is out of range for field
<em>[% field_descs.$field FILTER html %]</em>.
[% ELSIF error == "whine_access_denied" %]
[% title = "Access Denied" %]
Sorry, you aren't a member of the 'bz_canusewhines' group, and so
you aren't allowed to schedule whine reports.
[% ELSIF error == "zero_length_file" %]
[% title = "File Is Empty" %]
The file you are trying to attach is empty!

Просмотреть файл

@ -0,0 +1,96 @@
[%# 1.0@bugzilla.org %]
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
#%]
[%# INTERFACE:
# subject: subject line of message
# body: message body, shown before the query tables
# queries: array of hashes containing:
# bugs: array containing hashes of fieldnames->values for each bug
# title: the title given in the whine scheduling mechanism
# author: user object for the person who scheduled this whine
# recipient: user object for the intended recipient of the message
#%]
[% PROCESS global/variables.none.tmpl %]
[%# assignee_login_string is a literal string used for getting the
# assignee's name out of the bug data %]
[% SET assignee_login_string="map_assigned_to.login_name" %]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>
[[% terms.Bugzilla %]] [% subject FILTER html %]
</title>
</head>
<body bgcolor="#FFFFFF">
<p align="left">
[% body FILTER html %]
</p>
<p align="left">
[% IF author.login == recipient.login %]
<a href="[%+ Param('urlbase') FILTER html %]editwhines.cgi">Click
here to edit your whine schedule</a>
[% ELSE %]
This search was scheduled by [% author.login FILTER html %].
[% END %]
</p>
[% FOREACH query=queries %]
<h2>[%+ query.title FILTER html %]</h2>
<table width="100%">
<tr>
<th align="left">ID</th>
<th align="left">Sev</th>
<th align="left">Pri</th>
<th align="left">Plt</th>
<th align="left">Assignee</th>
<th align="left">Status</th>
<th align="left">Resolution</th>
<th align="left">Summary</th>
</tr>
[% FOREACH bug=query.bugs %]
<tr>
<td align="left"><a href="[%+ Param('urlbase') FILTER html %]show_bug.cgi?id=
[%- bug.bug_id %]">[% bug.bug_id %]</a></td>
<td align="left">[% bug.bug_severity FILTER html %]</td>
<td align="left">[% bug.priority FILTER html %]</td>
<td align="left">[% bug.rep_platform FILTER html %]</td>
<td align="left">[% bug.$assignee_login_string FILTER html %]</td>
<td align="left">[% bug.bug_status FILTER html %]</td>
<td align="left">[% bug.resolution FILTER html %]</td>
<td align="left">[% bug.short_desc FILTER html %]</td>
</tr>
[% END %]
</table>
[% END %]
</body>
</html>

Просмотреть файл

@ -0,0 +1,69 @@
[%# 1.0@bugzilla.org %]
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
#%]
[%# INTERFACE:
# subject: subject line of message
# body: message body, shown before the query tables
# queries: array of hashes containing:
# bugs: array containing hashes of fieldnames->values for each bug
# title: the title given in the whine scheduling mechanism
# author: user object for the person who scheduled this whine
# recipient: user object for the intended recipient of the message
#%]
[% PROCESS global/variables.none.tmpl %]
[%# assignee_login_string is a literal string used for getting the
# assignee's name out of the bug data %]
[% SET assignee_login_string="map_assigned_to.login_name" %]
[% body %]
[% IF author.login == recipient.login %]
To edit your whine schedule, visit the following URL:
[%+ Param('urlbase') %]editwhines.cgi
[% ELSE %]
This search was scheduled by [% author.login %].
[% END %]
[% FOREACH query=queries %]
[%+ query.title +%]
[%+ "-" FILTER repeat(query.title.length) %]
[% FOREACH bug=query.bugs %]
[% terms.Bug +%] [%+ bug.bug_id %]:
[%+ Param('urlbase') %]show_bug.cgi?id=[% bug.bug_id +%]
Priority: [%+ bug.priority -%]
Severity: [%+ bug.bug_severity -%]
Platform: [%+ bug.rep_platform %]
Assignee: [%+ bug.$assignee_login_string %]
Status: [%+ bug.bug_status %]
[%- IF bug.resolution -%] Resolution: [% bug.resolution -%]
[%- END %]
Summary: [% bug.short_desc %]
[% END %]
[% END %]

Просмотреть файл

@ -0,0 +1,52 @@
[%# 1.0@bugzilla.org %]
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
#%]
[%# INTERFACE:
# subject: subject line of message
# alternatives: array of hashes containing:
# type: MIME type
# content: verbatim content
# boundary: a string that has been generated to be a unique boundary
# recipient: user object for the intended recipient of the message
# from: Bugzilla system email address
#%]
[% PROCESS global/variables.none.tmpl %]
From: [% from %]
To: [% recipient.login %]
Subject: [[% terms.Bugzilla %]] [% subject %]
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="[% boundary %]"
This is a MIME multipart message. It is possible that your mail program
doesn't quite handle these properly. Some or all of the information in this
message may be unreadable.
[% FOREACH part=alternatives %]
--[% boundary %]
Content-type: [% part.type +%]
[%+ part.content %]
[%+ END %]

Просмотреть файл

@ -0,0 +1,406 @@
[%# 1.0@bugzilla.org %]
[%# -*- mode: html -*- %]
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
#%]
[%# INTERFACE:
# events: hash, keyed by event_id number. Values are anonymous hashes of:
# schedule: array of hashes containing schedule info:
# day: value in day column
# time: value selected in time column
# mailto: recipient's email address
# queries: as with schedule, an anonymous array containing hashes of:
# name: the named query's name
# title: title to be displayed on the results
# sort: integer that sets execution order on named queries
#%]
[% PROCESS global/variables.none.tmpl %]
[% title = "Set up whining" %]
[% PROCESS global/header.html.tmpl %]
<p>
"Whining" is when [% terms.Bugzilla %] executes a saved query at a regular interval
and sends the resulting list of [% terms.bugs %] via email.
</p>
<p>
To set up a new whine event, click "Add a new event." Enter a subject line
for the message that will be sent, along with a block of text that will
accompany the [% terms.bug %] list in the body of the message.
</p>
<p>
Schedules are added to an event by clicking on "Add a new schedule." A schedule
consists of a day, a time of day or interval of times
(e.g., every 15 minutes), and a target email address that may or may not be
alterable, depending on your privileges. Events may have more than one schedule
in order to run at multiple times or for different users.
</p>
<p>
Queries come from saved searches, which are created by executing a <a
href="query.cgi">search</a>, then telling [% terms.Bugzilla %] to remember
the search under a particular name. Add a query by clicking "Add a new
query", and select the desired saved search name under "Search" and add a
title for the [% terms.bug %] table. The optional number entered under
"Sort" will determine the execution order (lowest to highest) if multiple
queries are listed. If you check "One message per [% terms.bug %]," each [%
terms.bug %] that matches the search will be sent in its own email message.
</p>
<form method="post" action="editwhines.cgi">
[%# This hidden submit button must be here to set default behavior when
the user presses return on a form input field #%]
<input type="submit" value="Update / Commit" name="commit"
style="visibility: hidden">
<input type="hidden" name="update" value="1">
[% FOREACH event = events %]
<table cellspacing="2px" cellpadding="2px" border="0" width="100%"
style="border: 1px solid;">
<tr>
<th align="left" bgcolor="#FFFFFF" colspan="2">
Event:
</th>
<td align="right">
<input type="submit" value="Remove Event"
name="remove_event_[% event.key %]">
</td>
</tr>
<tr>
<td valign="top" align="right">
Email subject line:
</td>
<td>
<input type="text" name="event_[% event.key %]_subject"
size="60" maxlength="128" value="
[%- event.value.subject FILTER html %]">
</td>
</tr>
<tr>
<td valign="top" align="right">
Descriptive text sent within whine message:
</td>
<td>
<textarea name="event_[% event.key %]_body"
rows="5" cols="80">
[% event.value.body FILTER html %]</textarea>
</td>
</tr>
[% IF event.value.schedule.size == 0 %]
<tr>
<td valign="top" align="right">
Schedule:
</td>
<td align="left" bgcolor="#FFEEEE">
Not scheduled to run<br>
<input type="submit" value="Add a new schedule"
name="add_schedule_[% event.key %]">
</td>
</tr>
[% ELSE %]
<tr>
<td valign="top" align="right">
Schedule:
</td>
<td align="left" bgcolor="#EEFFEE">
<table>
<tr>
<th>
Interval
</th>
[% IF mail_others %]
<th>
Mail to
</th>
[% END %]
</tr>
[% FOREACH schedule = event.value.schedule %]
<tr>
<td align="left">
[%# these hidden fields allow us to compare old values instead
of reading the database to tell if a field has changed %]
<input type="hidden" value="[% schedule.day FILTER html %]"
name="orig_day_[% schedule.id %]">
<input type="hidden" value="[% schedule.time FILTER html %]"
name="orig_time_[% schedule.id %]">
[% PROCESS day_field val=schedule.day %]
[% PROCESS time_field val=schedule.time %]
</td>
<td align="left">
[% IF mail_others %]
<input type="hidden" name="orig_mailto_[% schedule.id %]"
value="[% schedule.mailto FILTER html %]">
<input type="text" name="mailto_[% schedule.id %]"
value="[% schedule.mailto FILTER html %]" size="30">
[% END %]
</td>
<td align="left">
<input type="submit" value="Remove"
name="remove_schedule_[% schedule.id %]">
</td>
</tr>
[% END %]
<tr>
<td>
<input type="submit" value="Add a new schedule"
name="add_schedule_[% event.key %]">
</td>
</tr>
</table>
</td>
</tr>
[% END %]
[% IF event.value.queries.size == 0 %]
<tr>
<td valign="top" align="right">
Queries:
</td>
<td align="left" colspan="1">
No queries <br>
<input type="submit" value="Add a new query" name="add_query_[% event.key %]">
</td>
<td align="right" valign="bottom">
<input type="submit" value="Update / Commit" name="commit">
</td>
</tr>
[% ELSE %]
<tr>
<td valign="top" align="right">
Queries:
</td>
<td align="left">
<table>
<tr>
<th>Sort</th>
<th>Search</th>
<th>Title</th>
</tr>
[% FOREACH query = event.value.queries %]
<tr>
<td align="left">
<input type="text" name="query_sort_[% query.id %]"
size="3" value="[% query.sort %]">
<input type="hidden" value="[% query.sort %]"
name="orig_query_sort_[% query.id %]">
</td>
<td align="left">
<input type="hidden" value="[% query.name FILTER html %]"
name="orig_query_name_[% query.id %]">
[% PROCESS query_field thisquery=query.name %]
</td>
<td align="left">
<input type="hidden" value="[% query.title FILTER html %]"
name="orig_query_title_[% query.id %]">
<input type="text" name="query_title_[% query.id %]"
size="50" value="[% query.title FILTER html %]"
maxlength="64">
</td>
<td align="left">
<input type="hidden" value="[% query.onemailperbug FILTER html %]"
name="orig_query_onemailperbug_[% query.id %]">
<input type="checkbox" [% IF query.onemailperbug == 1 %]
checked [% END %]name="query_onemailperbug_
[% query.id %]">
One message per [% terms.bug %]
</td>
<td align="right">
<input type="submit" value="Remove"
name="remove_query_[% query.id %]">
</td>
</tr>
[% END %]
<tr>
<td colspan="3">
<input type="submit" value="Add a new query"
name="add_query_[% event.key %]">
</td>
</tr>
</table>
</td>
<td align="right" valign="bottom">
<input type="submit" value="Update / Commit" name="commit">
</td>
</tr>
[% END %]
</table>
[% END %]
<p align="left">
<input type="submit" value="Add a new event" name="add_event">
</p>
</form>
[% PROCESS global/footer.html.tmpl %]
[% BLOCK query_field +%]
[% IF available_queries.size > 0 %]
<select name="query_name_[% query.id %]">
[% FOREACH q = available_queries %]
<option [% "selected" IF q == thisquery %] value="[% q FILTER html %]">
[% q FILTER html %]
</option>
[% END %]
</select>
[% ELSE %]
Please visit the <a href="query.cgi">Search</a> page and save a query
[% END %]
[%+ END %]
[% BLOCK day_field +%]
<select name="day_[% schedule.id %]">
[%
options = [
['All', 'Each day', ],
['MF', 'Monday through Friday', ],
['Sun', 'Sunday', ],
['Mon', 'Monday', ],
['Tue', 'Tuesday', ],
['Wed', 'Wednesday', ],
['Thu', 'Thursday', ],
['Fri', 'Friday', ],
['Sat', 'Saturday', ],
['1', 'On the 1st of the month', ],
['2', 'On the 2nd of the month', ],
['3', 'On the 3rd of the month', ],
['4', 'On the 4th of the month', ],
['5', 'On the 5th of the month', ],
['6', 'On the 6th of the month', ],
['7', 'On the 7th of the month', ],
['8', 'On the 8th of the month', ],
['9', 'On the 9th of the month', ],
['10', 'On the 10th of the month', ],
['11', 'On the 11th of the month', ],
['12', 'On the 12th of the month', ],
['13', 'On the 13th of the month', ],
['14', 'On the 14th of the month', ],
['15', 'On the 15th of the month', ],
['16', 'On the 16th of the month', ],
['17', 'On the 17th of the month', ],
['18', 'On the 18th of the month', ],
['19', 'On the 19th of the month', ],
['20', 'On the 20th of the month', ],
['21', 'On the 21st of the month', ],
['22', 'On the 22nd of the month', ],
['23', 'On the 23rd of the month', ],
['24', 'On the 24th of the month', ],
['25', 'On the 25th of the month', ],
['26', 'On the 26th of the month', ],
['27', 'On the 27th of the month', ],
['28', 'On the 28th of the month', ],
['29', 'On the 29th of the month', ],
['30', 'On the 30th of the month', ],
['31', 'On the 31st of the month', ],
['last', 'Last day of the month', ],
]
%]
[% FOREACH option = options %]
<option value="[% option.0 %]"
[%- IF val == option.0 +%] selected[% END %]>
[%- option.1 -%]
</option>
[% END %]
</select>
[%+ END %]
[% BLOCK time_field +%]
<select name="time_[% schedule.id %]">
[%
options = [
[ '0', 'at midnight', ],
[ '1', 'at 01:00', ],
[ '2', 'at 02:00', ],
[ '3', 'at 03:00', ],
[ '4', 'at 04:00', ],
[ '5', 'at 05:00', ],
[ '6', 'at 06:00', ],
[ '7', 'at 07:00', ],
[ '8', 'at 08:00', ],
[ '9', 'at 09:00', ],
[ '10', 'at 10:00', ],
[ '11', 'at 11:00', ],
[ '12', 'at 12:00', ],
[ '13', 'at 13:00', ],
[ '14', 'at 14:00', ],
[ '15', 'at 15:00', ],
[ '16', 'at 16:00', ],
[ '17', 'at 17:00', ],
[ '18', 'at 18:00', ],
[ '19', 'at 19:00', ],
[ '20', 'at 20:00', ],
[ '21', 'at 21:00', ],
[ '22', 'at 22:00', ],
[ '23', 'at 23:00', ],
[ '60min', 'every hour', ],
[ '30min', 'every 30 minutes', ],
[ '15min', 'every 15 minutes', ],
]
%]
[% FOREACH option = options %]
<option value="[% option.0 %]"
[%- IF val == option.0 +%] selected[% END %]>
[%- option.1 -%]
</option>
[% END %]
</select>
[%+ END %]

648
webtools/bugzilla/whine.pl Executable file
Просмотреть файл

@ -0,0 +1,648 @@
#!/usr/bin/perl -wT
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
################################################################################
# Script Initialization
################################################################################
use strict;
use lib ".";
require "globals.pl";
use Bugzilla::Config qw(:DEFAULT $datadir);
use Bugzilla::Constants;
use Bugzilla::Search;
use Bugzilla::User;
# create some handles that we'll need
my $template = Bugzilla->template;
my $dbh = Bugzilla->dbh;
my $sth;
# These statement handles should live outside of their functions in order to
# allow the database to keep their SQL compiled.
my $sth_run_queries =
$dbh->prepare("SELECT " .
"id, query_name, title, onemailperbug " .
"FROM whine_queries " .
"WHERE eventid=? " .
"ORDER BY sortkey");
my $sth_get_query =
$dbh->prepare("SELECT query FROM namedqueries " .
"WHERE userid = ? AND name = ?");
# get the event that's scheduled with the lowest run_next value
my $sth_next_scheduled_event = $dbh->prepare(
"SELECT " .
" whine_schedules.eventid, " .
" whine_events.owner_userid, " .
" whine_events.subject, " .
" whine_events.body " .
"FROM whine_schedules " .
"LEFT JOIN whine_events " .
" ON whine_events.id = whine_schedules.eventid " .
"WHERE run_next <= NOW() " .
"ORDER BY run_next LIMIT 1"
);
# get all pending schedules matching an eventid
my $sth_schedules_by_event = $dbh->prepare(
"SELECT id, mailto_userid " .
"FROM whine_schedules " .
"WHERE eventid=? AND run_next <= NOW()"
);
################################################################################
# Main Body Execution
################################################################################
# This script needs to check through the database for schedules that have
# run_next set to NULL, which means that schedule is new or has been altered.
# It then sets it to run immediately if the schedule entry has it running at
# an interval like every hour, otherwise to the appropriate day and time.
# After that, it looks over each user to see if they have schedules that need
# running, then runs those and generates the email messages.
# exit quietly if the system is shut down
if (Param('shutdownhtml')) {
exit;
}
# Send whines from the maintainer address. It's not a good idea to use
# the whine creator address because the admin can make more use of bounces and
# other replies.
my $fromaddress = Param('maintainer');
if ($fromaddress !~ Param('emailregexp')) {
die "Cannot run. " .
"The maintainer email address has not been properly set!\n";
}
# Check the nomail file for users who should not receive mail
my %nomail;
if (open(NOMAIL, '<', "$datadir/nomail")) {
while (<NOMAIL>) {
$nomail{trim($_)} = 1;
}
}
# get the current date and time from the database
$sth = $dbh->prepare( 'SELECT DATE_FORMAT( NOW(), "%y,%m,%e,%w,%k,%i")');
$sth->execute;
my ($now_year, $now_month, $now_day, $now_weekday, $now_hour, $now_minute) =
split(',', $sth->fetchrow_array);
$sth->finish;
my @daysinmonth = qw(0 31 28 31 30 31 30 31 31 30 31 30 31);
# Alter February in case of a leap year. This simple way to do it only
# applies if you won't be looking at February of next year, which whining
# doesn't need to do.
if (($now_year % 4 == 0) &&
(($now_year % 100 != 0) || ($now_year % 400 == 0))) {
$daysinmonth[2] = 29;
}
# run_day can contain either a calendar day (1, 2, 3...), a day of the week
# (Mon, Tue, Wed...), a range of days (All, MF), or 'last' for the last day of
# the month.
#
# run_time can contain either an hour (0, 1, 2...) or an interval
# (60min, 30min, 15min).
#
# We go over each uninitialized schedule record and use its settings to
# determine what the next time it runs should be
my $sched_h = $dbh->prepare("SELECT id, run_day, run_time " .
"FROM whine_schedules " .
"WHERE run_next IS NULL" );
$sched_h->execute();
while (my ($schedule_id, $day, $time) = $sched_h->fetchrow_array) {
# fill in some defaults in case they're blank
$day ||= '0';
$time ||= '0';
# If this schedule is supposed to run today, we see if it's supposed to be
# run at a particular hour. If so, we set it for that hour, and if not,
# it runs at an interval over the course of a day, which means we should
# set it to run immediately.
if (&check_today($day)) {
# Values that are not entirely numeric are intervals, like "30min"
if ($time !~ /^\d+$/) {
# set it to now
$sth = $dbh->prepare( "UPDATE whine_schedules " .
"SET run_next=NOW() " .
"WHERE id=?");
$sth->execute($schedule_id);
}
# A time greater than now means it still has to run today
elsif ($time >= $now_hour) {
# set it to today + number of hours
$sth = $dbh->prepare( "UPDATE whine_schedules " .
"SET run_next=DATE_ADD(CURRENT_DATE(), INTERVAL ? HOUR) " .
"WHERE id=?");
$sth->execute($time, $schedule_id);
}
# the target time is less than the current time
else { # set it for the next applicable day
my $nextdate = &get_next_date($day);
$sth = $dbh->prepare( "UPDATE whine_schedules " .
"SET run_next=" .
"DATE_ADD(?, INTERVAL ? HOUR) " .
"WHERE id=?");
$sth->execute($nextdate, $time, $schedule_id);
}
}
# If the schedule is not supposed to run today, we set it to run on the
# appropriate date and time
else {
my $target_date = &get_next_date($day);
# If configured for a particular time, set it to that, otherwise
# midnight
my $target_time = ($time =~ /^\d+$/) ? $time : 0;
$sth = $dbh->prepare( "UPDATE whine_schedules " .
"SET run_next=DATE_ADD(?, INTERVAL ? HOUR) " .
"WHERE id=?");
$sth->execute($target_date, $target_time, $schedule_id);
}
}
$sched_h->finish();
# get_next_event
#
# This function will:
# 1. Lock whine_schedules
# 2. Grab the most overdue pending schedules on the same event that must run
# 3. Update those schedules' run_next value
# 4. Unlock the table
# 5. Return an event hashref
#
# The event hashref consists of:
# eventid - ID of the event
# author - user object for the event's creator
# users - array of user objects for recipients
# subject - Subject line for the email
# body - the text inserted above the bug lists
sub get_next_event {
my $event = {};
# Loop until there's something to return
until (scalar keys %{$event}) {
$dbh->do("LOCK TABLE " .
"whine_schedules WRITE, " .
"whine_events READ, " .
"profiles READ, " .
"groups READ, " .
"user_group_map READ");
# Get the event ID for the first pending schedule
$sth_next_scheduled_event->execute;
my $fetched = $sth_next_scheduled_event->fetch;
$sth_next_scheduled_event->finish;
return undef unless $fetched;
my ($eventid, $owner_id, $subject, $body) = @{$fetched};
my $owner = Bugzilla::User->new($owner_id);
my $whineatothers = $owner->in_group('bz_canusewhineatothers');
my %user_objects; # Used for keeping track of who has been added
# Get all schedules that match that event ID and are pending
$sth_schedules_by_event->execute($eventid);
# Add the users from those schedules to the list
while (my $row = $sth_schedules_by_event->fetch) {
my ($sid, $mailto) = @{$row};
# Only bother doing any work if this user has whine permission
if ($owner->in_group('bz_canusewhines')) {
if (not defined $user_objects{$mailto}) {
if ($mailto == $owner_id) {
$user_objects{$mailto} = $owner;
}
elsif ($whineatothers) {
$user_objects{$mailto} = Bugzilla::User->new($mailto);
}
}
}
reset_timer($sid);
}
$dbh->do("UNLOCK TABLES");
# Only set $event if the user is allowed to do whining
if ($owner->in_group('bz_canusewhines')) {
my @users = values %user_objects;
$event = {
'eventid' => $eventid,
'author' => $owner,
'mailto' => \@users,
'subject' => $subject,
'body' => $body,
};
}
}
return $event;
}
# Run the queries for each event
#
# $event:
# eventid (the database ID for this event)
# author (user object for who created the event)
# mailto (array of user objects for mail targets)
# subject (subject line for message)
# body (text blurb at top of message)
while (my $event = get_next_event) {
my $eventid = $event->{'eventid'};
# We loop for each target user because some of the queries will be using
# subjective pronouns
Bugzilla->switch_to_shadow_db();
for my $target (@{$event->{'mailto'}}) {
my $args = {
'subject' => $event->{'subject'},
'body' => $event->{'body'},
'eventid' => $event->{'eventid'},
'author' => $event->{'author'},
'recipient' => $target,
'from' => $fromaddress,
};
# run the queries for this schedule
my $queries = run_queries($args);
# check to make sure there is something to output
my $there_are_bugs = 0;
for my $query (@{$queries}) {
$there_are_bugs = 1 if scalar @{$query->{'bugs'}};
}
next unless $there_are_bugs;
$args->{'queries'} = $queries;
mail($args);
}
Bugzilla->switch_to_main_db();
}
################################################################################
# Functions
################################################################################
# The mail and run_queries functions use an anonymous hash ($args) for their
# arguments, which are then passed to the templates.
#
# When run_queries is run, $args contains the following fields:
# - body Message body defined in event
# - from Bugzilla system email address
# - queries array of hashes containing:
# - bugs: array of hashes mapping fieldnames to values for this bug
# - title: text title given to this query in the whine event
# - schedule_id integer id of the schedule being run
# - subject Subject line for the message
# - recipient user object for the recipient
# - author user object of the person who created the whine event
#
# In addition, mail adds two more fields to $args:
# - alternatives array of hashes defining mime multipart types and contents
# - boundary a MIME boundary generated using the process id and time
#
sub mail {
my $args = shift;
# Don't send mail to someone on the nomail list.
return if $nomail{$args->{'recipient'}->{'login'}};
my $msg = ''; # it's a temporary variable to hold the template output
$args->{'alternatives'} ||= [];
# put together the different multipart mime segments
$template->process("whine/mail.txt.tmpl", $args, \$msg)
or die($template->error());
push @{$args->{'alternatives'}},
{
'content' => $msg,
'type' => 'text/plain',
};
$msg = '';
$template->process("whine/mail.html.tmpl", $args, \$msg)
or die($template->error());
push @{$args->{'alternatives'}},
{
'content' => $msg,
'type' => 'text/html',
};
$msg = '';
# now produce a ready-to-mail mime-encoded message
$args->{'boundary'} = "-----=====-----" . $$ . "--" . time() . "-----";
$template->process("whine/multipart-mime.txt.tmpl", $args, \$msg)
or die($template->error());
my $sendmailparam =
Param('sendmailnow') ? '' : "-ODeliveryMode=deferred";
open SENDMAIL, "|/usr/lib/sendmail $sendmailparam -t -i"
or die "Can't open sendmail";
print SENDMAIL $msg;
close SENDMAIL;
delete $args->{'boundary'};
delete $args->{'alternatives'};
}
# run_queries runs all of the queries associated with a schedule ID, adding
# the results to $args or mailing off the template if a query wants individual
# messages for each bug
sub run_queries {
my $args = shift;
my $return_queries = [];
$sth_run_queries->execute($args->{'eventid'});
my $queries = {};
for (@{$sth_run_queries->fetchall_arrayref}) {
$queries->{$_->[0]} = {
'name' => $_->[1],
'title' => $_->[2],
'onemailperbug' => $_->[3],
'bugs' => [],
};
}
for my $query_id (keys %{$queries}) {
my $thisquery = $queries->{$query_id};
next unless $thisquery->{'name'}; # named query is blank
my $savedquery = get_query($thisquery->{'name'}, $args->{'author'});
next unless $savedquery; # silently ignore missing queries
# Execute the saved query
my @searchfields = (
'bugs.bug_id',
'bugs.bug_severity',
'bugs.priority',
'bugs.rep_platform',
'bugs.assigned_to',
'bugs.bug_status',
'bugs.resolution',
'bugs.short_desc',
'map_assigned_to.login_name',
);
# A new Bugzilla::CGI object needs to be created to allow
# Bugzilla::Search to execute a saved query. It's exceedingly weird,
# but that's how it works.
my $searchparams = new Bugzilla::CGI($savedquery);
my $search = new Bugzilla::Search(
'fields' => \@searchfields,
'params' => $searchparams,
'user' => $args->{'recipient'}, # the search runs as the recipient
);
my $sqlquery = $search->getSQL();
$sth = $dbh->prepare($sqlquery);
$sth->execute;
while (my @row = $sth->fetchrow_array) {
my $bug = {};
for my $field (@searchfields) {
my $fieldname = $field;
$fieldname =~ s/^bugs\.//; # No need for bugs.whatever
$bug->{$fieldname} = shift @row;
}
if ($thisquery->{'onemailperbug'}) {
$args->{'queries'} = [
{
'name' => $thisquery->{'name'},
'title' => $thisquery->{'title'},
'bugs' => [ $bug ],
},
];
mail($args);
delete $args->{'queries'};
}
else { # It belongs in one message with any other lists
push @{$thisquery->{'bugs'}}, $bug;
}
}
unless ($thisquery->{'onemailperbug'}) {
push @{$return_queries}, $thisquery;
}
}
return $return_queries;
}
# get_query gets the namedquery. It's similar to LookupNamedQuery (in
# buglist.cgi), but doesn't care if a query name really exists or not, since
# individual named queries might go away without the whine_queries that point
# to them being removed.
sub get_query {
my ($name, $user) = @_;
my $qname = $name;
$sth_get_query->execute($user->{'id'}, $qname);
my $fetched = $sth_get_query->fetch;
$sth_get_query->finish;
return $fetched ? $fetched->[0] : '';
}
# check_today gets a run day from the schedule and sees if it matches today
# a run day value can contain any of:
# - a three-letter day of the week
# - a number for a day of the month
# - 'last' for the last day of the month
# - 'All' for every day
# - 'MF' for every weekday
sub check_today {
my $run_day = shift;
if (($run_day eq 'MF')
&& ($now_weekday > 0)
&& ($now_weekday < 6)) {
return 1;
}
elsif (
length($run_day) == 3 &&
index("SunMonTueWedThuFriSat", $run_day)/3 == $now_weekday) {
return 1;
}
elsif (($run_day eq 'All')
|| (($run_day eq 'last') &&
($now_day == $daysinmonth[$now_month] ))
|| ($run_day eq $now_day)) {
return 1;
}
return 0;
}
# reset_timer sets the next time a whine is supposed to run, assuming it just
# ran moments ago. Its only parameter is a schedule ID.
#
# reset_timer does not lock the whine_schedules table. Anything that calls it
# should do that itself.
sub reset_timer {
my $schedule_id = shift;
$sth = $dbh->prepare( "SELECT run_day, run_time FROM whine_schedules " .
"WHERE id=?" );
$sth->execute($schedule_id);
my ($run_day, $run_time) = $sth->fetchrow_array;
my $run_today = 0;
my $minute_offset = 0;
# If the schedule is to run today, and it runs many times per day,
# it shall be set to run immediately.
$run_today = &check_today($run_day);
if (($run_today) && ($run_time !~ /^\d+$/)) {
# The default of 60 catches any bad value
my $minute_interval = 60;
if ($run_time =~ /^(\d+)min$/i) {
$minute_interval = $1;
}
# set the minute offset to the next interval point
$minute_offset = $minute_interval - ($now_minute % $minute_interval);
}
elsif (($run_today) && ($run_time > $now_hour)) {
# timed event for later today
# (This should only happen if, for example, an 11pm scheduled event
# didn't happen until after midnight)
$minute_offset = (60 * ($run_time - $now_hour)) - $now_minute;
}
else {
# it's not something that runs later today.
$minute_offset = 0;
# Set the target time if it's a specific hour
my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0;
my $nextdate = &get_next_date($run_day);
$sth = $dbh->prepare( "UPDATE whine_schedules " .
"SET run_next=DATE_ADD(?, INTERVAL ? HOUR) " .
"WHERE id=?");
$sth->execute($nextdate, $target_time, $schedule_id);
return;
}
# Scheduling is done in terms of whole minutes, so we use DATE_SUB() to
# drop the seconds from the time.
if ($minute_offset > 0) {
$sth = $dbh->prepare("UPDATE whine_schedules " .
"SET run_next = " .
"DATE_SUB(DATE_ADD(NOW(), INTERVAL ? MINUTE), " .
"INTERVAL SECOND(NOW()) SECOND) " .
"WHERE id=?");
$sth->execute($minute_offset, $schedule_id);
} else {
# The minute offset is zero or less, which is not supposed to happen.
# This is a kind of safeguard against infinite loops. NULL schedules
# will not be available to get_next_event until they are rescheduled.
$sth = $dbh->prepare("UPDATE whine_schedules " .
"SET run_next = NULL " .
"WHERE id=?");
$sth->execute($schedule_id);
# complain to STDERR
print STDERR "Bad minute_offset for schedule ID $schedule_id\n";
}
}
# get_next_date determines the difference in days between now and the next
# time a schedule should run, excluding today
#
# It takes a run_day argument (see check_today, above, for an explanation),
# and returns an SQL date
sub get_next_date {
my $day = shift;
my $add_days = 0;
if ($day eq 'All') {
$add_days = 1;
}
elsif ($day eq 'last') {
# next_date should contain the last day of this month, or next month
# if it's today
if ($daysinmonth[$now_month] == $now_day) {
my $month = $now_month + 1;
$month = 1 if $month > 12;
$add_days = $daysinmonth[$month] + 1;
}
else {
$add_days = $daysinmonth[$now_month] - $now_day;
}
}
elsif ($day eq 'MF') { # any day Monday through Friday
if ($now_weekday < 5) { # Sun-Thurs
$add_days = 1;
}
elsif ($now_weekday == 5) { # Friday
$add_days = 3;
}
else { # it's 6, Saturday
$add_days = 2;
}
}
elsif ($day !~ /^\d+$/) { # A specific day of the week
# The default is used if there is a bad value in the database, in
# which case we mark it to a less-popular day (Sunday)
my $day_num = 0;
if (length($day) == 3) {
$day_num = (index("SunMonTueWedThuFriSat", $day)/3) or 0;
}
$add_days = $day_num - $now_weekday;
if ($add_days < 0) { # it's next week
$add_days += 7;
}
}
else { # it's a number, so we set it for that calendar day
$add_days = $day - $now_day;
# If it's already beyond that day this month, set it to the next one
if ($add_days < 0) {
$add_days += $daysinmonth[$now_month];
}
}
# Get a date in whatever format the database will accept
$sth = $dbh->prepare("SELECT DATE_ADD(CURRENT_DATE(), INTERVAL ? DAY)");
$sth->execute($add_days);
return $sth->fetch->[0];
}