bugzilla-4intranet/Bugzilla/FixWorktimePage.pm

394 lines
16 KiB
Perl
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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__