added csv importer for timesheet and project data (#1216)

This commit is contained in:
Kevin Papst
2019-11-23 22:02:55 +01:00
committed by GitHub
parent 0f16f42887
commit 817a43b178
9 changed files with 1158 additions and 107 deletions

View File

@@ -27,6 +27,7 @@
"kevinpapst/adminlte-bundle": "~3.0",
"kimai/kimai2-composer": "^0.1",
"laravolt/avatar": "^3.0",
"league/csv": "^9.4",
"mpdf/mpdf": "^7.1",
"nelmio/api-doc-bundle": "^3.2",
"nelmio/cors-bundle": "^1.5",

297
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "348a459e96d09528ed01477c95c8dd6a",
"content-hash": "89fb45fe0706c25775a066f5f6138d62",
"packages": [
{
"name": "beberlei/doctrineextensions",
"version": "v1.2.3",
"version": "v1.2.4",
"source": {
"type": "git",
"url": "https://github.com/beberlei/DoctrineExtensions.git",
"reference": "a8747a2aef34f602c365cf2efd1abc94c5ae7e0a"
"reference": "5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/a8747a2aef34f602c365cf2efd1abc94c5ae7e0a",
"reference": "a8747a2aef34f602c365cf2efd1abc94c5ae7e0a",
"url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139",
"reference": "5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139",
"shasum": ""
},
"require": {
@@ -58,7 +58,7 @@
"doctrine",
"orm"
],
"time": "2019-07-25T15:08:53+00:00"
"time": "2019-11-22T14:30:56+00:00"
},
{
"name": "behat/transliterator",
@@ -502,16 +502,16 @@
},
{
"name": "doctrine/doctrine-bundle",
"version": "1.11.2",
"version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "28101e20776d8fa20a00b54947fbae2db0d09103"
"reference": "a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/28101e20776d8fa20a00b54947fbae2db0d09103",
"reference": "28101e20776d8fa20a00b54947fbae2db0d09103",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115",
"reference": "a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115",
"shasum": ""
},
"require": {
@@ -519,11 +519,11 @@
"doctrine/doctrine-cache-bundle": "~1.2",
"jdorn/sql-formatter": "^1.2.16",
"php": "^7.1",
"symfony/config": "^3.4|^4.1",
"symfony/console": "^3.4|^4.1",
"symfony/dependency-injection": "^3.4|^4.1",
"symfony/doctrine-bridge": "^3.4|^4.1",
"symfony/framework-bundle": "^3.4|^4.1"
"symfony/config": "^3.4.30|^4.3.3",
"symfony/console": "^3.4.30|^4.3.3",
"symfony/dependency-injection": "^3.4.30|^4.3.3",
"symfony/doctrine-bridge": "^3.4.30|^4.3.3",
"symfony/framework-bundle": "^3.4.30|^4.3.3"
},
"conflict": {
"doctrine/orm": "<2.6",
@@ -533,14 +533,15 @@
"doctrine/coding-standard": "^6.0",
"doctrine/orm": "^2.6",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "7.0",
"symfony/cache": "^3.4|^4.1",
"phpunit/phpunit": "^7.5",
"symfony/cache": "^3.4.30|^4.3.3",
"symfony/phpunit-bridge": "^4.2",
"symfony/property-info": "^3.4|^4.1",
"symfony/validator": "^3.4|^4.1",
"symfony/web-profiler-bundle": "^3.4|^4.1",
"symfony/yaml": "^3.4|^4.1",
"twig/twig": "^1.34|^2.4"
"symfony/property-info": "^3.4.30|^4.3.3",
"symfony/twig-bridge": "^3.4|^4.1",
"symfony/validator": "^3.4.30|^4.3.3",
"symfony/web-profiler-bundle": "^3.4.30|^4.3.3",
"symfony/yaml": "^3.4.30|^4.3.3",
"twig/twig": "^1.34|^2.12"
},
"suggest": {
"doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
@@ -549,7 +550,7 @@
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.11.x-dev"
"dev-master": "1.12.x-dev"
}
},
"autoload": {
@@ -563,20 +564,20 @@
],
"authors": [
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Doctrine Project",
"homepage": "http://www.doctrine-project.org/"
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
"name": "Doctrine Project",
"homepage": "http://www.doctrine-project.org/"
}
],
"description": "Symfony DoctrineBundle",
@@ -587,7 +588,7 @@
"orm",
"persistence"
],
"time": "2019-06-04T07:35:05+00:00"
"time": "2019-11-19T11:42:20+00:00"
},
{
"name": "doctrine/doctrine-cache-bundle",
@@ -1093,33 +1094,35 @@
},
{
"name": "doctrine/orm",
"version": "v2.6.4",
"version": "v2.7.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/orm.git",
"reference": "b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43"
"reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/orm/zipball/b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43",
"reference": "b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43",
"url": "https://api.github.com/repos/doctrine/orm/zipball/4d763ca4c925f647b248b9fa01b5f47aa3685d62",
"reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62",
"shasum": ""
},
"require": {
"doctrine/annotations": "~1.5",
"doctrine/cache": "~1.6",
"doctrine/collections": "^1.4",
"doctrine/common": "^2.7.1",
"doctrine/dbal": "^2.6",
"doctrine/instantiator": "~1.1",
"doctrine/annotations": "^1.8",
"doctrine/cache": "^1.9.1",
"doctrine/collections": "^1.5",
"doctrine/common": "^2.11",
"doctrine/dbal": "^2.9.3",
"doctrine/event-manager": "^1.1",
"doctrine/instantiator": "^1.3",
"doctrine/persistence": "^1.2",
"ext-pdo": "*",
"php": "^7.1",
"symfony/console": "~3.0|~4.0"
"symfony/console": "^3.0|^4.0|^5.0"
},
"require-dev": {
"doctrine/coding-standard": "^5.0",
"phpunit/phpunit": "^7.5",
"symfony/yaml": "~3.4|~4.0"
"symfony/yaml": "^3.4|^4.0|^5.0"
},
"suggest": {
"symfony/yaml": "If you want to use YAML Metadata Mapping Driver"
@@ -1130,7 +1133,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.6.x-dev"
"dev-master": "2.7.x-dev"
}
},
"autoload": {
@@ -1165,12 +1168,12 @@
}
],
"description": "Object-Relational-Mapper for PHP",
"homepage": "http://www.doctrine-project.org",
"homepage": "https://www.doctrine-project.org/projects/orm.html",
"keywords": [
"database",
"orm"
],
"time": "2019-09-20T14:30:26+00:00"
"time": "2019-11-19T08:38:05+00:00"
},
{
"name": "doctrine/persistence",
@@ -2638,7 +2641,7 @@
},
{
"name": "illuminate/cache",
"version": "v6.5.1",
"version": "v6.5.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/cache.git",
@@ -2689,7 +2692,7 @@
},
{
"name": "illuminate/contracts",
"version": "v6.5.1",
"version": "v6.5.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -2733,16 +2736,16 @@
},
{
"name": "illuminate/support",
"version": "v6.5.1",
"version": "v6.5.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "236fa24d2e510c4800429cde89ed72e535051a5d"
"reference": "7a93f9e5a15bbf41236a8695fb738bf293f3c186"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/236fa24d2e510c4800429cde89ed72e535051a5d",
"reference": "236fa24d2e510c4800429cde89ed72e535051a5d",
"url": "https://api.github.com/repos/illuminate/support/zipball/7a93f9e5a15bbf41236a8695fb738bf293f3c186",
"reference": "7a93f9e5a15bbf41236a8695fb738bf293f3c186",
"shasum": ""
},
"require": {
@@ -2790,7 +2793,7 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2019-11-04T13:46:29+00:00"
"time": "2019-11-18T21:49:40+00:00"
},
{
"name": "intervention/image",
@@ -3308,6 +3311,75 @@
],
"time": "2019-09-04T14:39:26+00:00"
},
{
"name": "league/csv",
"version": "9.4.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "bf83acc23a7d60978fce36a384f97bf9d7d2ea0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/bf83acc23a7d60978fce36a384f97bf9d7d2ea0c",
"reference": "bf83acc23a7d60978fce36a384f97bf9d7d2ea0c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.0.10"
},
"require-dev": {
"ext-curl": "*",
"friendsofphp/php-cs-fixer": "^2.12",
"phpstan/phpstan": "^0.9.2",
"phpstan/phpstan-phpunit": "^0.9.4",
"phpstan/phpstan-strict-rules": "^0.9.0",
"phpunit/phpunit": "^6.0"
},
"suggest": {
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "Csv data manipulation made easy in PHP",
"homepage": "http://csv.thephpleague.com",
"keywords": [
"csv",
"export",
"filter",
"import",
"read",
"write"
],
"time": "2019-10-17T06:05:32+00:00"
},
{
"name": "markbaker/complex",
"version": "1.4.7",
@@ -3668,16 +3740,16 @@
},
{
"name": "nelmio/api-doc-bundle",
"version": "v3.4.0",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
"reference": "279d20be784b9879ad0a12859efa3a301404f06c"
"reference": "f596adfb4d16e65d1a1941f907092a119d4c76cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/279d20be784b9879ad0a12859efa3a301404f06c",
"reference": "279d20be784b9879ad0a12859efa3a301404f06c",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f596adfb4d16e65d1a1941f907092a119d4c76cb",
"reference": "f596adfb4d16e65d1a1941f907092a119d4c76cb",
"shasum": ""
},
"require": {
@@ -3689,13 +3761,17 @@
"symfony/property-info": "^3.4|^4.0",
"zircote/swagger-php": "^2.0.9"
},
"conflict": {
"symfony/framework-bundle": "4.2.7"
},
"require-dev": {
"api-platform/core": "^2.1.0",
"doctrine/annotations": "^1.2",
"doctrine/common": "^2.4",
"friendsofsymfony/rest-bundle": "^2.0",
"jms/serializer": "^1.14|^3.0",
"jms/serializer-bundle": "^2.0|^3.0",
"sensio/framework-extra-bundle": "^3.0",
"sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0",
"symfony/asset": "^3.4|^4.0",
"symfony/browser-kit": "^3.4|^4.0",
"symfony/cache": "^3.4|^4.0",
@@ -3703,8 +3779,9 @@
"symfony/console": "^3.4|^4.0",
"symfony/dom-crawler": "^3.4|^4.0",
"symfony/form": "^3.4|^4.0",
"symfony/phpunit-bridge": "^3.4|^4.0",
"symfony/phpunit-bridge": "^3.4.24|^4.0",
"symfony/property-access": "^3.4|^4.0",
"symfony/routing": "^3.4|^4.0",
"symfony/stopwatch": "^3.4|^4.0",
"symfony/templating": "^3.4|^4.0",
"symfony/twig-bundle": "^3.4|^4.0",
@@ -3747,7 +3824,7 @@
"documentation",
"rest"
],
"time": "2019-02-18T16:27:10+00:00"
"time": "2019-11-21T17:18:16+00:00"
},
{
"name": "nelmio/cors-bundle",
@@ -3809,22 +3886,22 @@
},
{
"name": "nesbot/carbon",
"version": "2.26.0",
"version": "2.27.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "e01ecc0b71168febb52ae1fdc1cfcc95428e604e"
"reference": "13b8485a8690f103bf19cba64879c218b102b726"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e01ecc0b71168febb52ae1fdc1cfcc95428e604e",
"reference": "e01ecc0b71168febb52ae1fdc1cfcc95428e604e",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/13b8485a8690f103bf19cba64879c218b102b726",
"reference": "13b8485a8690f103bf19cba64879c218b102b726",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.1.8 || ^8.0",
"symfony/translation": "^3.4 || ^4.0"
"symfony/translation": "^3.4 || ^4.0 || ^5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
@@ -3839,6 +3916,9 @@
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
},
"laravel": {
"providers": [
"Carbon\\Laravel\\ServiceProvider"
@@ -3872,7 +3952,7 @@
"datetime",
"time"
],
"time": "2019-10-21T21:32:25+00:00"
"time": "2019-11-20T06:59:06+00:00"
},
{
"name": "ocramius/package-versions",
@@ -4351,16 +4431,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.9.0",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df"
"reference": "f734783d826bd84c3d54fcf7b71c37ab9bac4b04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/8dea03eaf60a349b6097e4bcad11f894668280df",
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/f734783d826bd84c3d54fcf7b71c37ab9bac4b04",
"reference": "f734783d826bd84c3d54fcf7b71c37ab9bac4b04",
"shasum": ""
},
"require": {
@@ -4378,19 +4458,19 @@
"ext-zip": "*",
"ext-zlib": "*",
"markbaker/complex": "^1.4",
"markbaker/matrix": "^1.1",
"markbaker/matrix": "^1.2",
"php": "^7.1",
"psr/simple-cache": "^1.0"
},
"require-dev": {
"dompdf/dompdf": "^0.8.0",
"friendsofphp/php-cs-fixer": "@stable",
"dompdf/dompdf": "^0.8.3",
"friendsofphp/php-cs-fixer": "^2.16",
"jpgraph/jpgraph": "^4.0",
"mpdf/mpdf": "^7.0.0",
"phpcompatibility/php-compatibility": "^8.0",
"mpdf/mpdf": "^8.0",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.5",
"squizlabs/php_codesniffer": "^3.3",
"tecnickcom/tcpdf": "^6.2"
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.3"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
@@ -4406,7 +4486,7 @@
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
"MIT"
],
"authors": [
{
@@ -4440,7 +4520,7 @@
"xls",
"xlsx"
],
"time": "2019-08-17T22:24:35+00:00"
"time": "2019-11-18T11:33:05+00:00"
},
{
"name": "phpoffice/phpword",
@@ -8941,26 +9021,29 @@
},
{
"name": "white-october/pagerfanta-bundle",
"version": "v1.2.4",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/whiteoctober/WhiteOctoberPagerfantaBundle.git",
"reference": "19bf14bf8c72e4205c9ca97028436701fe3991a6"
"reference": "5421d5e79b72495b34b745d37e4d341e6fa2190d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/whiteoctober/WhiteOctoberPagerfantaBundle/zipball/19bf14bf8c72e4205c9ca97028436701fe3991a6",
"reference": "19bf14bf8c72e4205c9ca97028436701fe3991a6",
"url": "https://api.github.com/repos/whiteoctober/WhiteOctoberPagerfantaBundle/zipball/5421d5e79b72495b34b745d37e4d341e6fa2190d",
"reference": "5421d5e79b72495b34b745d37e4d341e6fa2190d",
"shasum": ""
},
"require": {
"pagerfanta/pagerfanta": "^1.1.0|^2.0.0",
"php": ">=5.3",
"php": ">=5.3.3",
"symfony/framework-bundle": "~2.3|~3.0|~4.0",
"symfony/property-access": "~2.3|~3.0|~4.0",
"symfony/translation": "~2.3|~3.0|~4.0",
"symfony/twig-bundle": "~2.3|~3.0|~4.0"
},
"conflict": {
"twig/twig": "<1.34|>=2.0,<2.4"
},
"require-dev": {
"phpunit/phpunit": "~3.7|~4.0|^5.0",
"symfony/symfony": "~2.3|~3.0|~4.0"
@@ -8995,7 +9078,7 @@
"page",
"paging"
],
"time": "2019-02-14T08:42:52+00:00"
"time": "2019-11-21T17:13:04+00:00"
},
{
"name": "willdurand/jsonp-callback-validator",
@@ -10062,16 +10145,16 @@
},
{
"name": "nette/php-generator",
"version": "v3.2.3",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
"reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b"
"reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/php-generator/zipball/aea6e81437bb238e5f0e5b5ce06337433908e63b",
"reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b",
"url": "https://api.github.com/repos/nette/php-generator/zipball/4240fd7adf499138c07b814ef9b9a6df9f6d7187",
"reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187",
"shasum": ""
},
"require": {
@@ -10085,7 +10168,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
"dev-master": "3.3-dev"
}
},
"autoload": {
@@ -10117,7 +10200,7 @@
"php",
"scaffolding"
],
"time": "2019-07-05T13:01:56+00:00"
"time": "2019-11-22T11:12:11+00:00"
},
{
"name": "nette/robot-loader",
@@ -10897,16 +10980,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "7.0.8",
"version": "7.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f"
"reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
"reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf",
"reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf",
"shasum": ""
},
"require": {
@@ -10956,7 +11039,7 @@
"testing",
"xunit"
],
"time": "2019-09-17T06:24:36+00:00"
"time": "2019-11-20T13:55:58+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -11397,16 +11480,16 @@
},
{
"name": "sebastian/environment",
"version": "4.2.2",
"version": "4.2.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
"reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"shasum": ""
},
"require": {
@@ -11446,7 +11529,7 @@
"environment",
"hhvm"
],
"time": "2019-05-05T09:05:15+00:00"
"time": "2019-11-20T08:46:58+00:00"
},
{
"name": "sebastian/exporter",
@@ -12088,16 +12171,16 @@
},
{
"name": "symfony/phpunit-bridge",
"version": "v4.3.8",
"version": "v4.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/phpunit-bridge.git",
"reference": "c216b32261358a820bb4217eb3a20e3f437a484e"
"reference": "7daa9d4a2f457f9cae32f1c2406de1921dc86a79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c216b32261358a820bb4217eb3a20e3f437a484e",
"reference": "c216b32261358a820bb4217eb3a20e3f437a484e",
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/7daa9d4a2f457f9cae32f1c2406de1921dc86a79",
"reference": "7daa9d4a2f457f9cae32f1c2406de1921dc86a79",
"shasum": ""
},
"require": {
@@ -12107,7 +12190,7 @@
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
},
"suggest": {
"symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader"
"symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader"
},
"bin": [
"bin/simple-phpunit"
@@ -12115,7 +12198,7 @@
"type": "symfony-bridge",
"extra": {
"branch-alias": {
"dev-master": "4.3-dev"
"dev-master": "4.4-dev"
},
"thanks": {
"name": "phpunit/phpunit",
@@ -12149,7 +12232,7 @@
],
"description": "Symfony PHPUnit Bridge",
"homepage": "https://symfony.com",
"time": "2019-10-30T12:58:49+00:00"
"time": "2019-11-08T15:59:14+00:00"
},
{
"name": "symfony/process",

View File

@@ -0,0 +1,338 @@
<?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\Configuration\FormConfiguration;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Importer\InvalidFieldsException;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TeamRepository;
use App\Repository\UserRepository;
use League\Csv\Reader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* This command can change anytime, don't rely on its API for the future!
*
* @internal
* @codeCoverageIgnore
*/
class ImportProjectCommand extends Command
{
protected static $defaultName = 'kimai:import:customer';
private static $requiredHeader = [
'Name',
'Customer',
];
private static $supportedHeader = [
'Name',
'Customer',
'Comment',
'OrderNumber',
'OrderDate',
];
/**
* @var CustomerRepository
*/
private $customers;
/**
* @var ProjectRepository
*/
private $projects;
/**
* @var TeamRepository
*/
private $teams;
/**
* @var UserRepository
*/
private $users;
/**
* @var FormConfiguration
*/
private $configuration;
/**
* @var Customer[]
*/
private $customerCache = [];
/**
* The datetime of this import as formatted string.
*
* @var string
*/
private $dateTime = '';
/**
* Comment that will be added to new customers, projects and activities.
*
* @var string
*/
private $comment = '';
public function __construct(CustomerRepository $customers, ProjectRepository $projects, TeamRepository $teams, UserRepository $users, FormConfiguration $configuration)
{
parent::__construct();
$this->customers = $customers;
$this->projects = $projects;
$this->teams = $teams;
$this->users = $users;
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName(self::$defaultName)
->setDescription('Import projects from CSV file')
->setHelp(
'This command allows to import projects from a CSV file, creating customers (if not existing) and optional empty teams for each project.' . PHP_EOL .
'Imported customer will be matched by name and optionally created on the fly.' . PHP_EOL .
'Required column names: ' . implode(', ', self::$requiredHeader) . PHP_EOL .
'Supported column names: ' . implode(', ', self::$supportedHeader) . PHP_EOL
)
->addOption('teamlead', null, InputOption::VALUE_REQUIRED, 'If you want to create empty teams for each project, give the username of the teamlead to be assigned')
->addOption('delimiter', null, InputOption::VALUE_OPTIONAL, 'The CSV field delimiter', ',')
->addOption('comment', null, InputOption::VALUE_OPTIONAL, 'A description to be added to created customers and projects. %s will be replaced with the current datetime', 'Imported at %s')
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to be imported')
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Kimai importer: Projects');
$csvFile = $input->getArgument('file');
if (!file_exists($csvFile)) {
$io->error('File not existing: ' . $csvFile);
return 1;
}
if (!is_readable($csvFile)) {
$io->error('File cannot be read: ' . $csvFile);
return 2;
}
$this->dateTime = (new \DateTime())->format('Y.m.d H:i');
$this->comment = sprintf($input->getOption('comment'), $this->dateTime);
$csv = Reader::createFromPath($csvFile, 'r');
$csv->setDelimiter($input->getOption('delimiter'));
$csv->setHeaderOffset(0);
$header = $csv->getHeader();
// validate teamlead
$teamlead = $input->getOption('teamlead');
if (null !== $teamlead) {
$tmpUser = $this->users->findOneBy(['username' => $teamlead]);
if (null === $tmpUser) {
$tmpUser = $this->users->findOneBy(['email' => $teamlead]);
if (null === $tmpUser) {
$io->error(
sprintf(
'You requested to create empty teams for each project, but the given teamlead cannot be found.' . PHP_EOL .
'Please create a user with the name (or email) %s first, before continuing.' . PHP_EOL,
$teamlead
)
);
return 3;
}
}
$teamlead = $tmpUser;
}
if (!$this->validateHeader($header)) {
$io->error(
sprintf(
'Found invalid CSV header: %s' . PHP_EOL .
'Required fields: %s' . PHP_EOL .
'All supported fields: %s' . PHP_EOL,
implode(', ', $header),
implode(', ', self::$requiredHeader),
implode(', ', self::$supportedHeader)
)
);
return 4;
}
$records = $csv->getRecords();
$doImport = true;
$row = 1;
$errors = 0;
foreach ($records as $record) {
try {
$this->validateRow($record);
} catch (InvalidFieldsException $ex) {
$io->error(sprintf('Invalid row %s, invalid fields: %s', $row, implode(', ', $ex->getFields())));
$doImport = false;
$errors++;
}
$row++;
}
if (!$doImport) {
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
return 5;
}
$row = 0;
foreach ($records as $record) {
$row++;
try {
$customer = $this->getCustomer($record['Customer']);
$projectName = $record['Name'];
$project = new Project();
$project->setName($projectName);
$project->setCustomer($customer);
$comment = $this->comment;
if (isset($record['Comment']) && !empty($record['Comment'])) {
$comment = $record['Comment'];
}
$project->setComment($comment);
if (isset($record['OrderNumber']) && !empty($record['OrderNumber'])) {
$project->setOrderNumber($record['OrderNumber']);
}
if (isset($record['OrderDate']) && !empty($record['OrderDate'])) {
$project->setOrderDate($record['OrderDate']);
}
$team = null;
if (null !== $teamlead) {
$team = new Team();
$team->setName($projectName);
$team->setTeamLead($teamlead);
$this->teams->saveTeam($team);
}
$this->projects->saveProject($project);
if (null !== $team) {
$project->addTeam($team);
$team->addProject($project);
$this->teams->saveTeam($team);
$this->projects->saveProject($project);
}
} catch (\Exception $ex) {
$io->error(sprintf('Failed importing project row %s with: %s', $row, $ex->getMessage()));
return 6;
}
}
$io->success(sprintf('Imported %s rows', $row));
return 0;
}
private function getCustomer(string $customerName): Customer
{
if (!array_key_exists($customerName, $this->customerCache)) {
$tmpCustomer = $this->customers->findBy(['name' => $customerName]);
if (count($tmpCustomer) > 1) {
throw new \Exception(sprintf('Found multiple customers with the name: %s', $customerName));
} elseif (count($tmpCustomer) === 1) {
$tmpCustomer = $tmpCustomer[0];
}
if ($tmpCustomer instanceof Customer) {
$this->customerCache[$customerName] = $tmpCustomer;
}
}
if (array_key_exists($customerName, $this->customerCache)) {
return $this->customerCache[$customerName];
}
$customer = new Customer();
$customer->setName(sprintf($customerName, $this->dateTime));
$customer->setComment($this->comment);
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
$timezone = date_default_timezone_get();
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
$timezone = $this->configuration->getCustomerDefaultTimezone();
}
$customer->setTimezone($timezone);
$this->customers->saveCustomer($customer);
$this->customerCache[$customerName] = $customer;
return $customer;
}
/**
* @param array $row
* @return bool
* @throws InvalidFieldsException
*/
private function validateRow(array $row)
{
$fields = [];
foreach (self::$requiredHeader as $headerName) {
if (!isset($row[$headerName]) || empty($row[$headerName])) {
$fields[] = $headerName;
}
}
if (!empty($fields)) {
throw new InvalidFieldsException($fields);
}
return true;
}
private function validateHeader(array $header)
{
$fields = [];
foreach (self::$requiredHeader as $headerName) {
if (!in_array($headerName, $header)) {
$fields[] = $headerName;
}
}
return empty($fields);
}
}

View File

@@ -0,0 +1,515 @@
<?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\Configuration\FormConfiguration;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Tag;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Importer\InvalidFieldsException;
use App\Importer\UnknownUserException;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use App\Utils\Duration;
use League\Csv\Reader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* This command can change anytime, don't rely on its API for the future!
*
* @internal
* @codeCoverageIgnore
*/
class ImportTimesheetCommand extends Command
{
protected static $defaultName = 'kimai:import:timesheet';
public const DEFAULT_BEGIN = '00:00';
public const DEFAULT_CUSTOMER = 'Imported customer - %s';
private static $supportedHeader = [
'Date',
'From',
'To',
'Duration',
'Rate',
'User',
'Customer',
'Project',
'Activity',
'Description',
'Exported',
'Tags',
'Hourly rate',
'Fixed rate',
];
/**
* @var CustomerRepository
*/
private $customers;
/**
* @var ProjectRepository
*/
private $projects;
/**
* @var ActivityRepository
*/
private $activities;
/**
* @var UserRepository
*/
private $users;
/**
* @var TimesheetRepository
*/
private $timesheets;
/**
* @var FormConfiguration
*/
private $configuration;
/**
* @var Customer
*/
private $customerFallback;
/**
* @var Customer[]
*/
private $customerCache = [];
/**
* @var User[]
*/
private $userCache = [];
/**
* Comment that will be added to new customers, projects and activities.
*
* @var string
*/
private $comment = '';
/**
* The datetime of this import as formatted string.
*
* @var string
*/
private $dateTime = '';
/**
* @var string
*/
private $begin = self::DEFAULT_BEGIN;
public function __construct(CustomerRepository $customers, ProjectRepository $projects, ActivityRepository $activities, UserRepository $users, TimesheetRepository $timesheets, FormConfiguration $configuration)
{
parent::__construct();
$this->customers = $customers;
$this->projects = $projects;
$this->activities = $activities;
$this->users = $users;
$this->timesheets = $timesheets;
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName(self::$defaultName)
->setDescription('Import timesheets from CSV file')
->setHelp(
'This command allows to import timesheets from a CSV file, which are formatted like CSV exports.' . PHP_EOL .
'Imported customer, projects and activities will be matched by name.' . PHP_EOL .
'Supported columns names: ' . implode(', ', self::$supportedHeader) . PHP_EOL
)
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'The timezone to be used. Supports: "valid timezone names", the string "user" (using the configured users timezone) and the string "server" (PHP default timezone)', 'user')
->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'A customer ID or name to assign for empty entries. Defaults to creating a new customer which is used for all un-linked projects')
->addOption('activity', null, InputOption::VALUE_OPTIONAL, 'Whether new activities should be "global" or "project" specific. Allowed values are "global" and "project"', 'project')
->addOption('delimiter', null, InputOption::VALUE_OPTIONAL, 'The CSV field delimiter', ',')
->addOption('begin', null, InputOption::VALUE_OPTIONAL, 'Default begin if none was provided in the format HH:MM', self::DEFAULT_BEGIN)
->addOption('comment', null, InputOption::VALUE_OPTIONAL, 'A description to be added to created customers, projects and activities. %s will be replaced with the current datetime', 'Imported at %s')
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to be imported')
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Kimai importer: Timesheets');
$csvFile = $input->getArgument('file');
if (!file_exists($csvFile)) {
$io->error('File not existing: ' . $csvFile);
return 1;
}
if (!is_readable($csvFile)) {
$io->error('File cannot be read: ' . $csvFile);
return 2;
}
$this->dateTime = (new \DateTime())->format('Y.m.d H:i');
$this->comment = sprintf($input->getOption('comment'), $this->dateTime);
$this->begin = $input->getOption('begin');
$timezone = $input->getOption('timezone');
switch ($timezone) {
case 'server':
$timezone = new \DateTimeZone(date_default_timezone_get());
break;
case 'user':
// null means fetch from user
$timezone = null;
break;
default:
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $ex) {
$io->error('Invalid timezone given, import canceled.');
return 3;
}
break;
}
$activityType = $input->getOption('activity');
$allowedActivityTypes = ['project', 'global'];
if (!in_array($activityType, $allowedActivityTypes)) {
$io->error(sprintf('Invalid activity type "%s" given, allowed values are: %s', $activityType, implode(', ', $allowedActivityTypes)));
return 4;
}
$csv = Reader::createFromPath($csvFile, 'r');
$csv->setDelimiter($input->getOption('delimiter'));
$csv->setHeaderOffset(0);
$header = $csv->getHeader();
if (!$this->validateHeader($header)) {
$io->error(
sprintf(
'Found invalid CSV. The header: ' . PHP_EOL .
'%s' . PHP_EOL .
'did not match the expected structure: ' . PHP_EOL .
'%s',
implode(', ', $header),
implode(', ', self::$supportedHeader)
)
);
return 5;
}
$records = $csv->getRecords();
$doImport = true;
$row = 1;
$errors = 0;
// ======================= validate rows =======================
foreach ($records as $record) {
try {
$this->validateRow($record);
} catch (InvalidFieldsException $ex) {
$io->error(sprintf('Invalid row %s, invalid fields: %s', $row, implode(', ', $ex->getFields())));
$doImport = false;
$errors++;
}
try {
$user = $this->getUser($record['User']);
} catch (\Exception $ex) {
$io->error(sprintf('Unknown user %s in row %s', $record['User'], $row));
$doImport = false;
$errors++;
}
$row++;
}
if (!$doImport) {
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
return 5;
}
$durationParser = new Duration();
$row = 0;
foreach ($records as $record) {
$row++;
try {
$project = $this->getProject($record['Project'], $record['Customer'], $input->getOption('customer'));
$activity = $this->getActivity($record['Activity'], $project, $activityType);
$user = $this->getUser($record['User']);
$begin = null;
$end = null;
$duration = 0;
if (!empty($record['Duration'])) {
if (is_int($record['Duration'])) {
$duration = $record['Duration'];
} else {
$duration = $durationParser->parseDurationString($record['Duration']);
}
}
if (null === $timezone) {
$timezone = new \DateTimeZone($user->getTimezone());
}
if (empty($record['From']) && empty($record['To'])) {
$begin = new \DateTime($record['Date'] . ' ' . $this->begin, $timezone);
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} elseif (empty($record['From'])) {
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
$begin = (new \DateTime())->setTimezone($timezone)->setTimestamp($end->getTimestamp() - $duration);
} elseif (empty($record['To'])) {
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
} else {
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
}
$timesheet = new Timesheet();
$timesheet->setActivity($activity);
$timesheet->setProject($project);
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$timesheet->setUser($user);
$timesheet->setDescription($record['Description']);
$timesheet->setExported((bool) $record['Exported']);
if (!empty($record['Tags'])) {
foreach (explode(',', $record['Tags']) as $tag) {
if (empty($tag)) {
continue;
}
$timesheet->addTag((new Tag())->setName($tag));
}
}
if (!empty($record['Rate'])) {
$timesheet->setRate($record['Rate']);
}
if (!empty($record['Hourly rate'])) {
$timesheet->setHourlyRate($record['Hourly rate']);
}
if (!empty($record['Fixed rate'])) {
$timesheet->setFixedRate($record['Fixed rate']);
}
$this->timesheets->save($timesheet);
} catch (\Exception $ex) {
$io->error(sprintf('Failed importing timesheet row %s with: %s', $row, $ex->getMessage()));
return 6;
}
}
$io->success(sprintf('Imported %s rows', $row));
return 0;
}
private function getUser($user): User
{
if (!array_key_exists($user, $this->userCache)) {
$tmpUser = $this->users->findOneBy(['username' => $user]);
if (null === $tmpUser) {
$tmpUser = $this->users->findOneBy(['email' => $user]);
if (null === $tmpUser) {
throw new UnknownUserException($user);
}
}
$this->userCache[$user] = $tmpUser;
}
return $this->userCache[$user];
}
private function getActivity($activity, Project $project, $activityType): Activity
{
$tmpActivity = null;
$tmpActivities = $this->activities->findBy(['project' => $project, 'name' => $activity]);
if (count($tmpActivities) === 0) {
$tmpActivity = $this->activities->findOneBy(['project' => null, 'name' => $activity]);
} elseif (count($tmpActivities) === 1) {
$tmpActivity = $tmpActivities[0];
}
if (null === $tmpActivity) {
$tmpActivity = new Activity();
$tmpActivity->setName($activity);
$tmpActivity->setComment($this->comment);
if ($activityType === 'project') {
$tmpActivity->setProject($project);
}
$this->activities->saveActivity($tmpActivity);
}
return $tmpActivity;
}
private function getProject($project, $customer, $fallbackCustomer): Project
{
/** @var Customer $tmpCustomer */
$tmpCustomer = $this->getCustomer($customer, $fallbackCustomer);
/** @var Project $tmpProject */
$tmpProject = null;
/** @var Project[] $tmpProjects */
$tmpProjects = $this->projects->findBy(['name' => $project]);
if (count($tmpProjects) > 1) {
/** @var Project $prj */
foreach ($tmpProjects as $prj) {
if ($prj->getCustomer()->getName() !== $tmpCustomer->getName()) {
continue;
}
$tmpProject = $prj;
break;
}
} elseif (count($tmpProjects) === 1) {
$tmpProject = $tmpProjects[0];
}
if (null !== $tmpProject) {
if ($tmpProject->getCustomer()->getName() !== $tmpCustomer->getName()) {
$tmpProject = null;
}
}
if ($tmpProject === null) {
$tmpProject = new Project();
$tmpProject->setName($project);
$tmpProject->setComment($this->comment);
$tmpProject->setCustomer($tmpCustomer);
$this->projects->saveProject($tmpProject);
}
return $tmpProject;
}
private function getCustomer($customer, $fallback): Customer
{
if (!empty($customer)) {
if (!array_key_exists($customer, $this->customerCache)) {
$tmpCustomer = $this->customers->findBy(['name' => $customer]);
if (count($tmpCustomer) > 1) {
throw new \Exception(sprintf('Found multiple customers with the name: %s', $customer));
} elseif (count($tmpCustomer) === 1) {
$tmpCustomer = $tmpCustomer[0];
}
if ($tmpCustomer instanceof Customer) {
$this->customerCache[$customer] = $tmpCustomer;
}
}
if (array_key_exists($customer, $this->customerCache)) {
return $this->customerCache[$customer];
}
}
if (null === $this->customerFallback) {
$tmpFallback = null;
if (!empty($fallback)) {
if (is_int($customer)) {
$tmpFallback = $this->customers->find($fallback);
} else {
$tmpFallback = $this->customers->findOneBy(['name' => $fallback]);
}
}
if (null === $tmpFallback) {
$newName = self::DEFAULT_CUSTOMER;
if (!empty($fallback) && is_string($fallback)) {
$newName = $fallback;
}
$tmpFallback = new Customer();
$tmpFallback->setName(sprintf($newName, $this->dateTime));
$tmpFallback->setComment($this->comment);
$tmpFallback->setCountry($this->configuration->getCustomerDefaultCountry());
$timezone = date_default_timezone_get();
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
$timezone = $this->configuration->getCustomerDefaultTimezone();
}
$tmpFallback->setTimezone($timezone);
$this->customers->saveCustomer($tmpFallback);
}
$this->customerFallback = $tmpFallback;
}
return $this->customerFallback;
}
/**
* @param array $row
* @return bool
* @throws InvalidFieldsException
*/
private function validateRow(array $row)
{
$fields = [];
if (empty($row['Project'])) {
$fields[] = 'Project';
}
if (empty($row['Activity'])) {
$fields[] = 'Activity';
}
if (empty($row['Date'])) {
$fields[] = 'Date';
}
if ((empty($row['From']) || empty($row['To'])) && empty($row['Duration'])) {
$fields[] = 'Duration';
}
if (!empty($fields)) {
throw new InvalidFieldsException($fields);
}
return true;
}
private function validateHeader(array $header)
{
$result = array_diff(self::$supportedHeader, $header);
return empty($result);
}
}

View File

@@ -0,0 +1,28 @@
<?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\Importer;
use Throwable;
class InvalidFieldsException extends \Exception
{
private $fields = [];
public function __construct(array $fields, $code = 0, Throwable $previous = null)
{
parent::__construct(sprintf('Missing fields: %s', implode(', ', $fields)), $code, $previous);
$this->fields = $fields;
}
public function getFields(): array
{
return $this->fields;
}
}

View File

@@ -0,0 +1,31 @@
<?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\Importer;
use Throwable;
class UnknownUserException extends \Exception
{
/**
* @var string
*/
private $username;
public function __construct(string $username, $code = 0, Throwable $previous = null)
{
parent::__construct(sprintf('Unknown user: %s', $username), $code, $previous);
$this->username = $username;
}
public function getUsername(): string
{
return $this->username;
}
}

View File

@@ -231,6 +231,9 @@
"laravolt/avatar": {
"version": "3.0.0"
},
"league/csv": {
"version": "9.4.1"
},
"markbaker/complex": {
"version": "1.4.6"
},

View File

@@ -0,0 +1,26 @@
<?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\Tests\Importer;
use App\Importer\InvalidFieldsException;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Importer\InvalidFieldsException
*/
class InvalidFieldsExceptionTest extends TestCase
{
public function testException()
{
$sut = new InvalidFieldsException(['test', 'foo']);
self::assertEquals(['test', 'foo'], $sut->getFields());
self::assertEquals('Missing fields: test, foo', $sut->getMessage());
}
}

View File

@@ -0,0 +1,26 @@
<?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\Tests\Importer;
use App\Importer\UnknownUserException;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Importer\UnknownUserException
*/
class UnknownUserExceptionTest extends TestCase
{
public function testException()
{
$sut = new UnknownUserException('test');
self::assertEquals('test', $sut->getUsername());
self::assertEquals('Unknown user: test', $sut->getMessage());
}
}