convert timesheets to UTC with support for user timezone (#372)
This commit is contained in:
43
src/Command/BashExecutor.php
Normal file
43
src/Command/BashExecutor.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
class BashExecutor
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir;
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @return BashResult
|
||||
*/
|
||||
public function execute(string $command)
|
||||
{
|
||||
$exitCode = 0;
|
||||
|
||||
$command = rtrim($this->rootDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($command, DIRECTORY_SEPARATOR);
|
||||
|
||||
ob_start();
|
||||
passthru($command, $exitCode);
|
||||
$result = ob_get_clean();
|
||||
|
||||
return new BashResult($exitCode, $result);
|
||||
}
|
||||
}
|
||||
70
src/Command/BashResult.php
Normal file
70
src/Command/BashResult.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
class BashResult
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $exitCode;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @param string $exitCode
|
||||
* @param string $result
|
||||
*/
|
||||
public function __construct($exitCode, $result)
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExitCode(): string
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $exitCode
|
||||
* @return BashResult
|
||||
*/
|
||||
public function setExitCode(string $exitCode)
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getResult(): string
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $result
|
||||
* @return BashResult
|
||||
*/
|
||||
public function setResult(string $result)
|
||||
{
|
||||
$this->result = $result;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
147
src/Command/ConvertTimezoneCommand.php
Normal file
147
src/Command/ConvertTimezoneCommand.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* This command was added with v0.8.
|
||||
* Kimai saved the DateTime before with the local timezone, which can cause big problems when used in different environments.
|
||||
* You should convert all timesheet records that were saved with Kimai 2 directly, but NOT the ones migrated from Kimai v1.
|
||||
*
|
||||
* Please read https://github.com/kevinpapst/kimai2/pull/372 to find out more!
|
||||
*/
|
||||
class ConvertTimezoneCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:convert-timezone')
|
||||
->setDescription('Convert timesheet dates between timezones, only made for updates from 0.7 to 0.8')
|
||||
->setHelp('This command should only be required if you imported data from Kimai v1')
|
||||
->addOption('first-id', 'f', InputArgument::OPTIONAL, 'The database ID which should be converted first')
|
||||
->addOption('last-id', 'l', InputArgument::OPTIONAL, 'The database ID which should be converted last')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $start
|
||||
* @param null|string $end
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getTimesheets($start = null, $end = null)
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
|
||||
$qb->select('t');
|
||||
if (!empty($start)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.id', $start));
|
||||
}
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.id', $end));
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param \DateTimeZone $timeZone
|
||||
* @return Timesheet
|
||||
*/
|
||||
protected function convertTimesheet(Timesheet $timesheet, \DateTimeZone $timeZone): Timesheet
|
||||
{
|
||||
$oldTimezone = $timesheet->getTimezone();
|
||||
|
||||
if (null !== $timesheet->getBegin()) {
|
||||
$beginDate = clone $timesheet->getBegin();
|
||||
$beginDate->setTimezone($timeZone);
|
||||
$timesheet->setBegin($beginDate);
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
$endDate = clone $timesheet->getEnd();
|
||||
$endDate->setTimezone($timeZone);
|
||||
$timesheet->setEnd($endDate);
|
||||
}
|
||||
|
||||
$timesheet->setTimezone($oldTimezone);
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
Type::overrideType(Type::DATETIME, DateTimeType::class);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$start = $input->getOption('first-id');
|
||||
$end = $input->getOption('last-id');
|
||||
|
||||
$result = $this->getTimesheets($start, $end);
|
||||
$amount = count($result);
|
||||
|
||||
$answer = $io->ask(sprintf('This will update %s timesheet records, continue (y/n) ? ', $amount));
|
||||
|
||||
if ('y' !== $answer) {
|
||||
$io->text('Aborting.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$i = 0;
|
||||
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($result as $timesheet) {
|
||||
$timesheet = $this->convertTimesheet($timesheet, $utc);
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
if (++$i % 80 === 0) {
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
} else {
|
||||
$io->write('.');
|
||||
}
|
||||
}
|
||||
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
$io->writeln('');
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\DBAL\Configuration;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -132,6 +134,10 @@ class KimaiImporterCommand extends Command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
// do not convert the times, Kimai 1 stored them already in UTC
|
||||
Type::overrideType(Type::DATETIME, DateTimeType::class);
|
||||
|
||||
// don't calculate rates ... this was done in Kimai 1
|
||||
$this->deactivateLifecycleCallbacks($this->getDoctrine()->getConnection());
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
@@ -20,16 +20,22 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
*/
|
||||
class RunCodeSnifferCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var BashExecutor
|
||||
*/
|
||||
protected $executor;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir = '';
|
||||
|
||||
/**
|
||||
* @param BashExecutor $executor
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct($projectDirectory)
|
||||
public function __construct(BashExecutor $executor, string $projectDirectory)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -42,7 +48,7 @@ class RunCodeSnifferCommand extends Command
|
||||
$this
|
||||
->setName('kimai:phpcs')
|
||||
->setDescription('Run PHP_CodeSniffer to check for the projects coding style')
|
||||
->addOption('fix', null, InputOption::VALUE_NONE, 'Fix all found problems (risky: modifies your files)')
|
||||
->addOption('fix', null, InputOption::VALUE_NONE, 'Fix all found problems')
|
||||
->addOption('checkstyle', null, InputOption::VALUE_OPTIONAL, '')
|
||||
;
|
||||
}
|
||||
@@ -55,7 +61,6 @@ class RunCodeSnifferCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$filename = null;
|
||||
ob_start();
|
||||
|
||||
$args = [];
|
||||
if (!$input->getOption('fix')) {
|
||||
@@ -78,13 +83,11 @@ class RunCodeSnifferCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$exitCode = 0;
|
||||
passthru($this->rootDir . '/vendor/bin/php-cs-fixer fix ' . implode(' ', $args), $exitCode);
|
||||
$result = ob_get_clean();
|
||||
$result = $this->executor->execute('/vendor/bin/php-cs-fixer fix ' . implode(' ', $args));
|
||||
|
||||
$io->write($result);
|
||||
$io->write($result->getResult());
|
||||
|
||||
if ($exitCode > 0) {
|
||||
if ($result->getExitCode() > 0) {
|
||||
$io->error(
|
||||
'Found problems while checking your code styles' .
|
||||
(!empty($filename) ? '. Saved checkstyle data to: ' . $filename : '')
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
/**
|
||||
* Command used to run all integration tests.
|
||||
*/
|
||||
@@ -29,11 +27,10 @@ class RunIntegrationTestsCommand extends RunUnitTestsCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $directory
|
||||
* @return string
|
||||
*/
|
||||
protected function createPhpunitCmdLine($directory)
|
||||
protected function createPhpunitCmdLine()
|
||||
{
|
||||
return $this->rootDir . '/bin/phpunit --group integration ' . $directory;
|
||||
return '/bin/phpunit --group integration ' . $this->rootDir . '/tests';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,22 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
*/
|
||||
class RunUnitTestsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var BashExecutor
|
||||
*/
|
||||
protected $executor;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir;
|
||||
|
||||
/**
|
||||
* RunCodeSnifferCommand constructor.
|
||||
* @param BashExecutor $executor
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct($projectDirectory)
|
||||
public function __construct(BashExecutor $executor, string $projectDirectory)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -53,36 +58,24 @@ class RunUnitTestsCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->executeTests($io, '/tests');
|
||||
$result = $this->executor->execute($this->createPhpunitCmdLine());
|
||||
|
||||
$io->write($result->getResult());
|
||||
|
||||
if ($result->getExitCode() > 0) {
|
||||
$io->error('Found problems while running tests');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->success('All tests were successful');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $directory
|
||||
* @return string
|
||||
*/
|
||||
protected function createPhpunitCmdLine($directory)
|
||||
protected function createPhpunitCmdLine()
|
||||
{
|
||||
return $this->rootDir . '/bin/phpunit --exclude-group integration ' . $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
*/
|
||||
protected function executeTests(SymfonyStyle $io, $directory)
|
||||
{
|
||||
$directory = $this->rootDir . $directory;
|
||||
|
||||
$exitCode = 0;
|
||||
ob_start();
|
||||
passthru($this->createPhpunitCmdLine($directory), $exitCode);
|
||||
$result = ob_get_clean();
|
||||
|
||||
$io->write($result);
|
||||
|
||||
if ($exitCode > 0) {
|
||||
$io->error('Found problems while running tests at: ' . $directory);
|
||||
} else {
|
||||
$io->success('All tests performed good at: ' . $directory);
|
||||
}
|
||||
return '/bin/phpunit --exclude-group integration ' . $this->rootDir . '/tests';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user