diff --git a/src/Command/KimaiImporterCommand.php b/src/Command/KimaiImporterCommand.php index 3cb3d33f..e749dcd3 100644 --- a/src/Command/KimaiImporterCommand.php +++ b/src/Command/KimaiImporterCommand.php @@ -247,7 +247,7 @@ class KimaiImporterCommand extends Command $allImports += $counter; $io->success('Imported users: ' . $counter); } catch (\Exception $ex) { - $io->error('Failed to import users: ' . $ex->getMessage()); + $io->error('Failed to import users: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } @@ -257,7 +257,7 @@ class KimaiImporterCommand extends Command $allImports += $counter; $io->success('Imported customers: ' . $counter); } catch (\Exception $ex) { - $io->error('Failed to import customers: ' . $ex->getMessage()); + $io->error('Failed to import customers: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } @@ -267,7 +267,7 @@ class KimaiImporterCommand extends Command $allImports += $counter; $io->success('Imported projects: ' . $counter); } catch (\Exception $ex) { - $io->error('Failed to import projects: ' . $ex->getMessage()); + $io->error('Failed to import projects: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } @@ -277,7 +277,7 @@ class KimaiImporterCommand extends Command $allImports += $counter; $io->success('Imported activities: ' . $counter); } catch (\Exception $ex) { - $io->error('Failed to import activities: ' . $ex->getMessage()); + $io->error('Failed to import activities: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } @@ -287,7 +287,7 @@ class KimaiImporterCommand extends Command $allImports += $counter; $io->success('Imported timesheet records: ' . $counter); } catch (\Exception $ex) { - $io->error('Failed to import timesheet records: ' . $ex->getMessage()); + $io->error('Failed to import timesheet records: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } @@ -385,8 +385,10 @@ class KimaiImporterCommand extends Command protected function bytesHumanReadable($size) { $unit = ['b', 'kB', 'MB', 'GB']; + $i = floor(log($size, 1024)); + $a = (int) $i; - return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i]; + return @round($size / pow(1024, $i), 2) . ' ' . $unit[$a]; } /** @@ -422,12 +424,8 @@ class KimaiImporterCommand extends Command if ($errors->count() > 0) { /** @var \Symfony\Component\Validator\ConstraintViolation $error */ foreach ($errors as $error) { - $value = $error->getInvalidValue(); $io->error( - $error->getPropertyPath() - . ' (' . (is_array($value) ? implode(',', $value) : $value) . ')' - . "\n " - . $error->getMessage() + (string) $error ); } @@ -941,7 +939,7 @@ class KimaiImporterCommand extends Command ; if (!$this->validateImport($io, $timesheet)) { - throw new \Exception('Failed to validate timesheet record: ' . $timesheet->getId()); + throw new \Exception('Failed to validate timesheet record: ' . $oldRecord['timeEntryID']); } try { @@ -952,8 +950,7 @@ class KimaiImporterCommand extends Command } ++$counter; } catch (\Exception $ex) { - $io->error('Failed to create timesheet record: ' . $timesheet->getId()); - $io->error('Reason: ' . $ex->getMessage()); + $io->error('Failed to create timesheet record: ' . $ex->getMessage()); } $io->write('.'); diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 5440c080..b675f879 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -376,21 +376,21 @@ class Timesheet ->addViolation(); } - if ($activity->getVisible() === false) { + if (null === $this->getEnd() && $activity->getVisible() === false) { $context->buildViolation('Cannot start a disabled activity.') ->atPath('activity') ->setTranslationDomain('validators') ->addViolation(); } - if ($project->getVisible() === false) { + if (null === $this->getEnd() && $project->getVisible() === false) { $context->buildViolation('Cannot start a disabled project.') ->atPath('project') ->setTranslationDomain('validators') ->addViolation(); } - if ($project->getCustomer()->getVisible() === false) { + if (null === $this->getEnd() && $project->getCustomer()->getVisible() === false) { $context->buildViolation('Cannot start a disabled customer.') ->atPath('customer') ->setTranslationDomain('validators') diff --git a/src/Migrations/Version20181031220003.php b/src/Migrations/Version20181031220003.php index a1d368ae..fbf69515 100644 --- a/src/Migrations/Version20181031220003.php +++ b/src/Migrations/Version20181031220003.php @@ -56,10 +56,11 @@ final class Version20181031220003 extends AbstractMigration $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); } else { // project table + $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); // timesheet table $this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id'); - $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); } @@ -80,7 +81,12 @@ final class Version20181031220003 extends AbstractMigration $this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)'); $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)'); } else { + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649'); + $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096'); $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE'); } } @@ -94,6 +100,7 @@ final class Version20181031220003 extends AbstractMigration $timesheet = $this->getTableName('timesheet'); $projects = $this->getTableName('projects'); + $customers = $this->getTableName('customers'); if ($platform === 'sqlite') { // project table @@ -117,7 +124,9 @@ final class Version20181031220003 extends AbstractMigration $this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)'); } else { // project table + $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3'); $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE'); // timesheet table $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C'); $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet); diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php index ce190d2a..6f1707a9 100644 --- a/tests/Entity/TimesheetTest.php +++ b/tests/Entity/TimesheetTest.php @@ -121,6 +121,24 @@ class TimesheetTest extends AbstractEntityTest $this->assertHasViolationForField($entity, 'customer'); } + public function testValidationCustomerInvisibleDoesNotTriggerOnStoppedEntites() + { + $customer = (new Customer())->setVisible(false); + $project = (new Project())->setName('foo')->setCustomer($customer); + $activity = (new Activity())->setName('hello-world')->setProject($project); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ->setEnd(new \DateTime()) + ; + + $this->assertHasNoViolations($entity); + } + public function testValidationProjectInvisible() { $customer = new Customer(); @@ -138,6 +156,24 @@ class TimesheetTest extends AbstractEntityTest $this->assertHasViolationForField($entity, 'project'); } + public function testValidationProjectInvisibleDoesNotTriggerOnStoppedEntites() + { + $customer = new Customer(); + $project = (new Project())->setName('foo')->setCustomer($customer)->setVisible(false); + $activity = (new Activity())->setName('hello-world')->setProject($project); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ->setEnd(new \DateTime()) + ; + + $this->assertHasNoViolations($entity); + } + public function testValidationActivityInvisible() { $customer = new Customer(); @@ -155,6 +191,24 @@ class TimesheetTest extends AbstractEntityTest $this->assertHasViolationForField($entity, 'activity'); } + public function testValidationActivityInvisibleDoesNotTriggerOnStoppedEntites() + { + $customer = new Customer(); + $project = (new Project())->setName('foo')->setCustomer($customer); + $activity = (new Activity())->setName('hello-world')->setProject($project)->setVisible(false); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ->setEnd(new \DateTime()) + ; + + $this->assertHasNoViolations($entity); + } + public function testValidationEndNotEarlierThanBegin() { $entity = $this->getEntity(); diff --git a/var/docs/faq.md b/var/docs/faq.md index 1cdc4d25..02d74cfe 100644 --- a/var/docs/faq.md +++ b/var/docs/faq.md @@ -42,3 +42,19 @@ SQLite is a great database engine for testing, but when it comes to production u - It does not support ALTER TABLE commands and makes update procedures very clunky and problematic/errorsome (we still try to support updates, but they are heavy on large databases) - It does not support FOREIGN KEY constraints out of the box, which can lead to critical bugs when deleting activities/projects/customers + +## Dotenv::populate() must be an instance of Symfony\\Component\\Dotenv\\void + +If you encounter an error like this: + +``` +PHP Fatal error: Uncaught TypeError: Return value of Symfony\\Component\\Dotenv\\Dotenv::populate() must be an instance of Symfony\\Component\\Dotenv\\void, none returned in /var/www/kimai2/vendor/symfony/dotenv/Dotenv.php:95 +Stack trace: +#0 /var/www/kimai2/vendor/symfony/dotenv/Dotenv.php(57): Symfony\\Component\\Dotenv\\Dotenv->populate(Array) +#1 /var/www//kimai2/public/index.php(15): Symfony\\Component\\Dotenv\\Dotenv->load('/var/www/html/k...') +#2 {main}\n thrown in /var/www/kimai2/vendor/symfony/dotenv/Dotenv.php on line 95 + +``` + +you are running PHP 7.0. Probably you were able to install Kimai v2, because your PHP-CLI uses a different PHP version than your webserver. +Upgrade PHP and the error will be gone. \ No newline at end of file