394 lines
16 KiB
Perl
394 lines
16 KiB
Perl
#!/usr/bin/perl
|
||
# "Super-Fix-Worktime" page - allows to enter worktime for many bugs at once,
|
||
# even for past dates and/or for other users, if you have 'superworktime' privilege.
|
||
# Originally CustIS Bug 68921.
|
||
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
||
# Author: Vitaliy Filippov <vitalif@mail.ru>
|
||
|
||
package Bugzilla::FixWorktimePage;
|
||
|
||
use strict;
|
||
use POSIX;
|
||
|
||
use Bugzilla;
|
||
use Bugzilla::Constants;
|
||
use Bugzilla::Error;
|
||
use Bugzilla::Bug;
|
||
use Bugzilla::Util;
|
||
use Bugzilla::Token;
|
||
|
||
# Загрузить из бага bug_id время за период $from..$to
|
||
# в разрезе по пользователям, и вернуть в виде { user_id => work_time }
|
||
sub LoadTimes
|
||
{
|
||
my ($bug_id, $from, $to) = @_;
|
||
my $sql = 'SELECT who, SUM(work_time) wt FROM longdescs WHERE bug_id=?';
|
||
my @bind = ($bug_id);
|
||
if ($from)
|
||
{
|
||
$sql .= ' AND bug_when>=?';
|
||
push @bind, $from;
|
||
}
|
||
if ($to)
|
||
{
|
||
$sql .= ' AND bug_when<?';
|
||
push @bind, $to;
|
||
}
|
||
$sql .= ' GROUP BY who';
|
||
my $propo = Bugzilla->dbh->selectall_hashref($sql, 'who', undef, @bind);
|
||
$propo = { map { $_ => $propo->{$_}->{wt} } keys %$propo };
|
||
return $propo;
|
||
}
|
||
|
||
# Отмасштабировать время $propo = { key => work_time } так, чтобы
|
||
# сумма стала равна $newsum, а отдельные элементы не были меньше $min.
|
||
sub Scale
|
||
{
|
||
my ($propo, $newsum, $min) = @_;
|
||
if (scalar(keys %$propo) == 1)
|
||
{
|
||
# Если элемент один, размазывать там нечего
|
||
return { keys %$propo => $newsum };
|
||
}
|
||
my $sum = 0;
|
||
my $n = keys %$propo;
|
||
$sum += $propo->{$_} for keys %$propo;
|
||
my $new = {};
|
||
my $nt;
|
||
# Сортировка сама по себе не так важна, важно вначале
|
||
# обрабатывать меньшие значения, чтобы большие размазались корректно
|
||
# Граничный ошибочный случай - 1 баг с большим значением в начале
|
||
# и много мелких в конце - в этом случае сумма не сойдётся.
|
||
for (sort { $propo->{$a} <=> $propo->{$b} } keys %$propo)
|
||
{
|
||
$nt = $sum ? $propo->{$_}*$newsum/$sum : $newsum/$n;
|
||
$nt = POSIX::floor($nt*100+0.5)/100;
|
||
if (abs($nt) < $min || $newsum*$nt < 0)
|
||
{
|
||
# не размазываем время совсем уж мелкими суммами
|
||
# и размазываем только суммами того же знака, что и вся сумма
|
||
$sum -= $propo->{$_};
|
||
$n--;
|
||
next;
|
||
}
|
||
# корректируем ошибки округления, чтобы сумма всё равно сходилась
|
||
$sum -= $propo->{$_};
|
||
$newsum -= $nt;
|
||
$n--;
|
||
$new->{$_} = $nt;
|
||
}
|
||
return $new;
|
||
}
|
||
|
||
# Добавить к багу $bug (Bugzilla::Bug) на момент $timestamp,
|
||
# с комментарием $comment или "Fix worktime" рабочее время из
|
||
# $times = { $user_id => $work_time, ... }
|
||
# Возвращает 1 если всё нормально, 0 если обновление блокировано проверками.
|
||
# При этом _комментарий_ к багу добавляется задним числом от заданного пользователя,
|
||
# но _изменения_ (Show Bug History) логгируются всегда от имени текущего пользователя.
|
||
# Так сделано в Bugzilla::Bug и в этом даже есть некая логика (хотя бы можно посмотреть,
|
||
# из-под кого в реальности фиксировалось время).
|
||
sub AddWorktime
|
||
{
|
||
my ($bug, $comment, $timestamp, $times) = @_;
|
||
$comment ||= "Fix worktime";
|
||
|
||
my $sum = 0;
|
||
for (keys %$times)
|
||
{
|
||
next unless $times->{$_};
|
||
$sum += $times->{$_};
|
||
$bug->add_comment($comment, {
|
||
work_time => $times->{$_},
|
||
bug_when => $timestamp,
|
||
who => $_,
|
||
type => $timestamp lt $bug->delta_ts ? CMT_BACKDATED_WORKTIME : CMT_WORKTIME,
|
||
});
|
||
}
|
||
return 1 unless $sum;
|
||
|
||
my $remaining_time = $bug->remaining_time;
|
||
my $newrtime = $remaining_time - $sum;
|
||
$newrtime = 0 if $newrtime < 0;
|
||
$bug->set('remaining_time', $newrtime) if $newrtime != $remaining_time;
|
||
|
||
$bug->update();
|
||
|
||
if (@{$bug->{failed_checkers} || []} && !$bug->{passed_checkers})
|
||
{
|
||
return 0;
|
||
}
|
||
else
|
||
{
|
||
# stop bugmail
|
||
Bugzilla->dbh->do('UPDATE bugs SET lastdiffed=NOW() WHERE bug_id=?', undef, $bug->id);
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
# Используется только fill-day-worktime.cgi (старый Today Worktime)
|
||
sub FixWorktime
|
||
{
|
||
my ($bug, $wtime, $comment, $timestamp, $userid) = @_;
|
||
return 1 unless $wtime;
|
||
$bug = Bugzilla::Bug->check({ id => $bug, for_update => 1 }) unless ref $bug;
|
||
return 0 unless $bug;
|
||
return AddWorktime($bug, $comment, $timestamp, { ($userid || Bugzilla->user->id) => $wtime });
|
||
}
|
||
|
||
# Пропорциональное или равномерное распределение времени по пользователям
|
||
# с пропорцией, взятой из этого бага или подсунутой в $propo = { user_id => work_time }
|
||
sub DistributeWorktime
|
||
{
|
||
my ($bug, $t, $comment, $timestamp, $from, $to, $min_inc, $propo) = @_;
|
||
return 1 unless $t;
|
||
|
||
$propo ||= LoadTimes($bug->id, $from, $to);
|
||
return 1 unless %$propo;
|
||
|
||
my $times = Scale($propo, $t, $min_inc);
|
||
return AddWorktime($bug, $comment, $timestamp, $times);
|
||
}
|
||
|
||
# Разбираем дату
|
||
sub ParseWtDate
|
||
{
|
||
my ($wt_date) = @_;
|
||
# Списывать будем последней секундой дня, если дата в прошлом
|
||
$wt_date .= ' 23:59:59' if $wt_date !~ / /;
|
||
eval
|
||
{
|
||
$wt_date = datetime_from($wt_date);
|
||
if ($wt_date->epoch > time)
|
||
{
|
||
# Если время было не указано, а дата текущая, получится
|
||
# дата в будущем (23:59:59) - значит списываем текущим временем
|
||
$wt_date = undef;
|
||
}
|
||
else
|
||
{
|
||
$wt_date = $wt_date->ymd . ' ' . $wt_date->hms;
|
||
}
|
||
};
|
||
# Если не распарсилась - не будем втихаря списывать
|
||
# на текущее число, а сообщим об ошибке формата
|
||
if ($@)
|
||
{
|
||
ThrowUserError('illegal_date', {
|
||
date => $_[0],
|
||
format => 'YYYY-MM-DD HH:MM:SS',
|
||
});
|
||
}
|
||
return $wt_date;
|
||
}
|
||
|
||
# Ищем юзера, если больше одного - предлагаем выбор
|
||
sub MatchWtUser
|
||
{
|
||
my ($wt_user) = @_;
|
||
my $matches = Bugzilla::User::match_name($wt_user);
|
||
if (scalar(@$matches) != 1)
|
||
{
|
||
Bugzilla->cgi->delete('worktime_user');
|
||
my $vars = {};
|
||
$vars->{matches} = { 'worktime_user' => { $wt_user => { users => $matches } } };
|
||
$vars->{matchsuccess} = @$matches ? 1 : 0;
|
||
Bugzilla->template->process("global/confirm-user-match.html.tmpl", $vars)
|
||
|| ThrowTemplateError(Bugzilla->template->error());
|
||
exit;
|
||
}
|
||
return $matches->[0];
|
||
}
|
||
|
||
# Списать время из $times на набор багов с комментарием $comment на время $timestamp
|
||
# $times формата { bug_id => { user_id => work_time, ... }, ... }
|
||
sub MassAddWorktime
|
||
{
|
||
my ($times, $comment, $timestamp) = @_;
|
||
Bugzilla->request_cache->{checkers_hide_error} = 1;
|
||
my ($ok, $rollback) = 0;
|
||
my $bug;
|
||
for (keys %$times)
|
||
{
|
||
Bugzilla->dbh->bz_start_transaction();
|
||
$ok = 1;
|
||
if ($bug = Bugzilla::Bug->new({ id => $_, for_update => 1 }))
|
||
{
|
||
$ok = AddWorktime($bug, $comment, $timestamp, $times->{$_});
|
||
}
|
||
if ($ok)
|
||
{
|
||
# Юзеры хотят цельный коммит и построчную диагностику...
|
||
# Так что коммитим в SAVEPOINT, а потом, если хотя бы одно изменение
|
||
# не пройдёт, сделаем полный откат
|
||
Bugzilla->dbh->bz_commit_transaction();
|
||
}
|
||
else
|
||
{
|
||
# А если не OK, значит нас уже обломали Checkers'ы,
|
||
# а они сами откатывают транзакцию до последнего Savepoint'а
|
||
$rollback = 1;
|
||
}
|
||
}
|
||
return !$rollback;
|
||
}
|
||
|
||
# Подготовить время для списания:
|
||
# $times - введённые часы или пропорция по багам
|
||
# $tsfrom, $tsto - выбранный период времени
|
||
# $wt_user - выбранный пользователь
|
||
# $other_bug_id - откуда взять пропорцию участия пользователей
|
||
# $move_time - флаг "перенести время с other_bug_id"
|
||
# Возвращает хешреф в формате { bug_id => { user_id => время } }
|
||
sub PrepareWorktime
|
||
{
|
||
my ($times, $wt_user, $tsfrom, $tsto, $other_bug_id, $move_time, $min_inc) = @_;
|
||
my $user_times;
|
||
my $r = {};
|
||
if ($other_bug_id)
|
||
{
|
||
$user_times = LoadTimes($other_bug_id, $tsfrom, $tsto);
|
||
$user_times = { $wt_user->id => $user_times->{$wt_user->id} || 0 } if $wt_user;
|
||
my $sum = 0;
|
||
$sum += $_ for values %$user_times;
|
||
if (!$sum)
|
||
{
|
||
# Если хотели взять пропорцию из бага, а в него не списано время - ругнёмся
|
||
ThrowUserError('move_worktime_empty', {
|
||
bug_id => $other_bug_id,
|
||
from => $tsfrom,
|
||
to => $tsto,
|
||
who => $wt_user,
|
||
});
|
||
}
|
||
}
|
||
if ($wt_user)
|
||
{
|
||
# Списываем заданное время на одного участника
|
||
$r->{$_} = { $wt_user->id => $times->{$_} } for keys %$times;
|
||
}
|
||
elsif ($move_time && $other_bug_id)
|
||
{
|
||
# Если также отмечено move_time, то введённое значение времени игнорируется,
|
||
# берётся из $other_bug_id для каждого его участника за заданный период,
|
||
# распределяется по багам в соответствии с пропорцией $times, и потом
|
||
# и потом списывается с отрицательным знаком в $other_bug_id.
|
||
# Получается перенос времени пользователей с одного бага на множество других...
|
||
delete $times->{$other_bug_id};
|
||
my $proportion = $times;
|
||
$r = {};
|
||
for my $uid (keys %$user_times)
|
||
{
|
||
my $scaled = Scale($proportion, $user_times->{$uid}, $min_inc);
|
||
for (keys %$scaled)
|
||
{
|
||
$r->{$_}->{$uid} = $scaled->{$_};
|
||
}
|
||
$r->{$other_bug_id}->{$uid} = -$user_times->{$uid};
|
||
}
|
||
}
|
||
else
|
||
{
|
||
# Распределяем время по нескольким участникам
|
||
for (keys %$times)
|
||
{
|
||
$r->{$_} = Scale($user_times || LoadTimes($_, $tsfrom, $tsto), $times->{$_}, $min_inc);
|
||
}
|
||
}
|
||
return $r;
|
||
}
|
||
|
||
# Обработать запрос к SuperWorkTime
|
||
# $vars - выходные переменные для шаблона
|
||
# $args - входные параметры запроса
|
||
sub HandleSuperWorktime
|
||
{
|
||
my ($vars, $args) = @_;
|
||
my $template = Bugzilla->template;
|
||
$vars->{wt_admin} = Bugzilla->user->in_group('worktimeadmin');
|
||
# Обрабатываем списанное время, а потом делаем редирект на себя же
|
||
if ($args->{save_worktime})
|
||
{
|
||
# Проверяем токен
|
||
check_token_data($args->{token}, 'superworktime');
|
||
my ($wt_user, $wt_date);
|
||
my $comment = $args->{comment};
|
||
if ($vars->{wt_admin})
|
||
{
|
||
# Парсим дату, только если можем списывать задним числом
|
||
$wt_date = ParseWtDate($args->{worktime_date});
|
||
$wt_user = MatchWtUser($args->{worktime_user}) if $args->{worktime_user};
|
||
}
|
||
else
|
||
{
|
||
# Если мы не можем списывать задним юзером - форсим текущего юзера
|
||
$wt_user = Bugzilla->user;
|
||
}
|
||
# Заданный в поиске период времени
|
||
my ($tsfrom, $tsto) = ($args->{chfieldfrom} || '', $args->{chfieldto} || '');
|
||
# Точность распределения времени - время распределяется на части, не большие, чем $min_inc
|
||
my $min_inc = $args->{divide_min_inc};
|
||
# $other_bug_id - это баг, из которого берётся пропорция для расписывания времени по юзерам
|
||
my $other_bug_id = $args->{divide_other_bug_id};
|
||
trick_taint($_) for $tsfrom, $tsto, $other_bug_id;
|
||
my $times = {};
|
||
my $t;
|
||
foreach (map { /^wtime_(\d+)$/ } keys %$args)
|
||
{
|
||
$t = $args->{"wtime_$_"};
|
||
$times->{$_} = Bugzilla::Bug::ValidateTime($t, 'work_time') if $t;
|
||
}
|
||
# В транзакции сначала готовим, потом коммитим
|
||
Bugzilla->dbh->bz_start_transaction();
|
||
$times = PrepareWorktime($times, $wt_user, $tsfrom, $tsto, $other_bug_id, $args->{move_time}, $min_inc);
|
||
if ($args->{dry_run})
|
||
{
|
||
# Тестовый проход - не списываем, а только показываем результат
|
||
# Просто форму показать нельзя, т.к. может быть списание за нескольких участников,
|
||
# а на форме его отразить негде :(
|
||
Bugzilla->dbh->bz_commit_transaction();
|
||
my $user_times = {};
|
||
foreach my $bug_id (keys %$times)
|
||
{
|
||
foreach my $user_id (keys %{$times->{$bug_id}})
|
||
{
|
||
$user_times->{$user_id}->{$bug_id} = $times->{$bug_id}->{$user_id};
|
||
}
|
||
}
|
||
my $users = {};
|
||
foreach my $user (@{ Bugzilla::User->new_from_list([ keys %$user_times ]) })
|
||
{
|
||
$users->{$user->id} = $user;
|
||
}
|
||
$vars->{test_times_by_bug} = $times;
|
||
$vars->{test_times_by_user} = $user_times;
|
||
$vars->{users} = $users;
|
||
$vars->{round} = sub { ($_[0] < 0 ? -int(-$_[0]*100+0.5) : int($_[0]*100+0.5))/100 };
|
||
$template->process('worktime/dry-run.html.tmpl', $vars);
|
||
exit;
|
||
}
|
||
else
|
||
{
|
||
# Удаляем параметры
|
||
if (MassAddWorktime($times, $comment, $wt_date))
|
||
{
|
||
Bugzilla->dbh->bz_commit_transaction();
|
||
delete_token($args->{token});
|
||
}
|
||
else
|
||
{
|
||
# Цельный откат, если хотя бы одно изменение заблокировано проверками
|
||
Bugzilla->dbh->bz_rollback_transaction();
|
||
Bugzilla::CheckerUtils::show_checker_errors();
|
||
}
|
||
delete $args->{$_} for 'token', 'save_worktime', grep { /^wtime_(\d+)$/ } keys %$args;
|
||
print Bugzilla->cgi->redirect(-location => 'buglist.cgi?'.http_build_query($args));
|
||
exit;
|
||
}
|
||
}
|
||
}
|
||
|
||
1;
|
||
__END__
|