diff --git a/composer.json b/composer.json index d7aef26..ba0c179 100644 --- a/composer.json +++ b/composer.json @@ -46,8 +46,10 @@ "roave/security-advisories": "dev-master", "symfony/console": "^4.0", "tuupola/cors-middleware": "^0.7.0", + "zendframework/zend-cache": "^2.7", "zendframework/zend-code": "^3.3", "zendframework/zend-component-installer": "^2.1.1", + "zendframework/zend-config": "^3.1", "zendframework/zend-config-aggregator": "^1.0", "zendframework/zend-diactoros": "^1.7.1", "zendframework/zend-eventmanager": "^3.2", @@ -56,6 +58,7 @@ "zendframework/zend-expressive-helpers": "^5.0", "zendframework/zend-filter": "^2.7", "zendframework/zend-form": "^2.11", + "zendframework/zend-http": "^2.7", "zendframework/zend-hydrator": "^2.3", "zendframework/zend-json": "^3.1", "zendframework/zend-log": "^2.10", diff --git a/composer.lock b/composer.lock index c9c08d6..b45d51e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "eada4b0e60e35c9a3f5d5bcf347984fc", + "content-hash": "e84c04bc2ba63a0cee39cb04418a10ef", "packages": [ { "name": "behat/transliterator", @@ -1830,6 +1830,75 @@ ], "time": "2017-07-15T22:03:15+00:00" }, + { + "name": "zendframework/zend-cache", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-cache.git", + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.10.0", + "phpunit/phpunit": "^4.8", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-serializer": "^2.6", + "zendframework/zend-session": "^2.6.2" + }, + "suggest": { + "ext-apc": "APC or compatible extension, to use the APC storage adapter", + "ext-apcu": "APCU >= 5.1.0, to use the APCu storage adapter", + "ext-dba": "DBA, to use the DBA storage adapter", + "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", + "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", + "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-redis": "Redis, to use Redis storage adapter", + "ext-wincache": "WinCache, to use the WinCache storage adapter", + "ext-xcache": "XCache, to use the XCache storage adapter", + "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-session": "Zend\\Session component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" + }, + "zf": { + "component": "Zend\\Cache", + "config-provider": "Zend\\Cache\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides a generic way to cache any data", + "homepage": "https://github.com/zendframework/zend-cache", + "keywords": [ + "cache", + "zf2" + ], + "time": "2016-12-16T11:35:47+00:00" + }, { "name": "zendframework/zend-code", "version": "3.3.0", @@ -1935,6 +2004,66 @@ ], "time": "2018-03-21T16:53:56+00:00" }, + { + "name": "zendframework/zend-config", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-config.git", + "reference": "a12e4a592bf66d9629b84960e268f3752e53abe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-config/zipball/a12e4a592bf66d9629b84960e268f3752e53abe4", + "reference": "a12e4a592bf66d9629b84960e268f3752e53abe4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^5.6 || ^7.0", + "psr/container": "^1.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "conflict": { + "container-interop/container-interop": "<1.2.0" + }, + "require-dev": { + "malukenho/docheader": "^0.1.5", + "phpunit/phpunit": "^5.7 || ^6.0", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-filter": "^2.7.1", + "zendframework/zend-i18n": "^2.7.3", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.2.1" + }, + "suggest": { + "zendframework/zend-filter": "^2.7.1; install if you want to use the Filter processor", + "zendframework/zend-i18n": "^2.7.3; install if you want to use the Translator processor", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.2.1; if you need an extensible plugin manager for use with the Config Factory" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev", + "dev-develop": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Config\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "homepage": "https://github.com/zendframework/zend-config", + "keywords": [ + "config", + "zf2" + ], + "time": "2017-02-22T14:31:10+00:00" + }, { "name": "zendframework/zend-config-aggregator", "version": "1.1.0", @@ -2611,6 +2740,59 @@ ], "time": "2017-12-06T21:09:08+00:00" }, + { + "name": "zendframework/zend-http", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-http.git", + "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/78aa510c0ea64bfb2aa234f50c4f232c9531acfa", + "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-loader": "^2.5.1", + "zendframework/zend-stdlib": "^3.1 || ^2.7.7", + "zendframework/zend-uri": "^2.5.2", + "zendframework/zend-validator": "^2.10.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4.1 || ^5.7.15", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-config": "^3.1 || ^2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests", + "homepage": "https://github.com/zendframework/zend-http", + "keywords": [ + "ZendFramework", + "http", + "http client", + "zend", + "zf" + ], + "time": "2017-10-13T12:06:24+00:00" + }, { "name": "zendframework/zend-httphandlerrunner", "version": "1.0.1", @@ -2830,6 +3012,50 @@ ], "time": "2018-01-04T17:51:34+00:00" }, + { + "name": "zendframework/zend-loader", + "version": "2.5.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-loader.git", + "reference": "c5fd2f071bde071f4363def7dea8dec7393e135c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/c5fd2f071bde071f4363def7dea8dec7393e135c", + "reference": "c5fd2f071bde071f4363def7dea8dec7393e135c", + "shasum": "" + }, + "require": { + "php": ">=5.3.23" + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev", + "dev-develop": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Loader\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-loader", + "keywords": [ + "loader", + "zf2" + ], + "time": "2015-06-03T14:05:47+00:00" + }, { "name": "zendframework/zend-log", "version": "2.10.0", @@ -3080,6 +3306,53 @@ ], "time": "2018-03-15T14:10:32+00:00" }, + { + "name": "zendframework/zend-uri", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-uri.git", + "reference": "fb998b9487ea8c5f4aaac0e536190709bdd5353b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/fb998b9487ea8c5f4aaac0e536190709bdd5353b", + "reference": "fb998b9487ea8c5f4aaac0e536190709bdd5353b", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-escaper": "^2.5", + "zendframework/zend-validator": "^2.5" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.1 || ^5.7.15", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev", + "dev-develop": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "a component that aids in manipulating and validating ยป Uniform Resource Identifiers (URIs)", + "homepage": "https://github.com/zendframework/zend-uri", + "keywords": [ + "uri", + "zf2" + ], + "time": "2018-04-10T17:08:10+00:00" + }, { "name": "zendframework/zend-validator", "version": "2.10.2", diff --git a/config/config.php b/config/config.php index 26e7106..5d3caf1 100644 --- a/config/config.php +++ b/config/config.php @@ -13,6 +13,7 @@ $cacheConfig = [ ]; $aggregator = new ConfigAggregator([ + \Zend\Cache\ConfigProvider::class, \Zend\Log\ConfigProvider::class, \Zend\Form\ConfigProvider::class, \Zend\InputFilter\ConfigProvider::class, diff --git a/config/routes.php b/config/routes.php index 52e238b..e40cf31 100644 --- a/config/routes.php +++ b/config/routes.php @@ -44,4 +44,8 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac $app->put('/api/slide-position/{id:\d+}', App\Handler\SlidePositionHandler::class,'api.slide.position'); $app->route('/api/slide[/{id:\d+}]', App\Handler\SlideHandler::class)->setName('api.slide'); + + $app->get('/avatars/{signum}', App\Handler\AvatarHandler::class,'avatar.image'); + $app->get('/api/kanban[/{filterId:\d+}]', App\Handler\KanbanHandler::class,'api.team.kanban'); + }; diff --git a/public/assets/riddler.png b/public/assets/riddler.png new file mode 100644 index 0000000..5db5c17 Binary files /dev/null and b/public/assets/riddler.png differ diff --git a/public/avatars/enorsos b/public/avatars/enorsos new file mode 100644 index 0000000..49281ae Binary files /dev/null and b/public/avatars/enorsos differ diff --git a/public/avatars/epetfid b/public/avatars/epetfid new file mode 100644 index 0000000..15d4fc8 Binary files /dev/null and b/public/avatars/epetfid differ diff --git a/public/avatars/ethzto b/public/avatars/ethzto new file mode 100644 index 0000000..6510673 Binary files /dev/null and b/public/avatars/ethzto differ diff --git a/public/avatars/etorist b/public/avatars/etorist new file mode 100644 index 0000000..2a821d3 Binary files /dev/null and b/public/avatars/etorist differ diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php index bd66889..add1920 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -40,9 +40,16 @@ class ConfigProvider Handler\TeamHandler::class => Handler\TeamHandlerFactory::class, Handler\SlideHandler::class => Handler\SlideHandlerFactory::class, Handler\SlidePositionHandler::class => Handler\SlidePositionHandlerFactory::class, + Handler\AvatarHandler::class => Handler\AvatarHandlerFactory::class, + Handler\KanbanHandler::class =>Handler\KanbanHandlerFactory::class, Service\TeamService::class => Service\TeamServiceFactory::class, Service\SlideManager::class => Service\SlideManagerFactory::class, + Service\AvatarService::class => Service\AvatarServiceFactory::class, + Service\JiraCollectorService::class => Service\JiraCollectorServiceFactory::class, + + \Zend\Http\Client::class => Service\HttpClientFactory::class, + 'service.cache' => Service\CacheServiceFactory::class, ], ]; } diff --git a/src/App/Entity/JiraAssignee.php b/src/App/Entity/JiraAssignee.php new file mode 100644 index 0000000..dff1c13 --- /dev/null +++ b/src/App/Entity/JiraAssignee.php @@ -0,0 +1,135 @@ +signum; + } + + /** + * @param string $signum + * @return JiraAssignee + */ + public function setSignum(string $signum): JiraAssignee + { + $this->signum = $signum; + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * @return JiraAssignee + */ + public function setName(string $name): JiraAssignee + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getEmail(): string + { + return $this->email; + } + + /** + * @param string $email + * @return JiraAssignee + */ + public function setEmail(string $email): JiraAssignee + { + $this->email = $email; + return $this; + } + + /** + * @return string + */ + public function getAvatar(): string + { + return $this->avatar; + } + + /** + * @param string $avatar + * @return JiraAssignee + */ + public function setAvatar(string $avatar): JiraAssignee + { + $this->avatar = $avatar; + return $this; + } + + /** + * @return bool + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @param bool $active + * @return JiraAssignee + */ + public function setActive(bool $active): JiraAssignee + { + $this->active = $active; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'signum' => $this->getSignum(), + 'name' => $this->getName(), + 'email' => $this->getEmail(), + 'avatar' => $this->getAvatar(), + 'active' => $this->isActive(), + ]; + } +} diff --git a/src/App/Entity/JiraIssueType.php b/src/App/Entity/JiraIssueType.php new file mode 100644 index 0000000..9300da1 --- /dev/null +++ b/src/App/Entity/JiraIssueType.php @@ -0,0 +1,89 @@ +name; + } + + /** + * @param string $name + * @return JiraIssueType + */ + public function setName(string $name): JiraIssueType + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @param string $description + * @return JiraIssueType + */ + public function setDescription(string $description): JiraIssueType + { + $this->description = $description; + return $this; + } + + /** + * @return string + */ + public function getIcon(): string + { + return $this->icon; + } + + /** + * @param string $icon + * @return JiraIssueType + */ + public function setIcon(string $icon): JiraIssueType + { + $this->icon = $icon; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'icon' => $this->getIcon(), + ]; + } +} diff --git a/src/App/Entity/JiraStatus.php b/src/App/Entity/JiraStatus.php new file mode 100644 index 0000000..f595b28 --- /dev/null +++ b/src/App/Entity/JiraStatus.php @@ -0,0 +1,65 @@ +name; + } + + /** + * @param string $name + * @return JiraStatus + */ + public function setName(string $name): JiraStatus + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getColor(): string + { + return $this->color; + } + + /** + * @param string $color + * @return JiraStatus + */ + public function setColor(string $color): JiraStatus + { + $this->color = $color; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'name' => $this->getName(), + 'color' => $this->getColor(), + ]; + } +} diff --git a/src/App/Entity/KanbanBoard.php b/src/App/Entity/KanbanBoard.php new file mode 100644 index 0000000..1d717d4 --- /dev/null +++ b/src/App/Entity/KanbanBoard.php @@ -0,0 +1,260 @@ + self::PRIO_TRIVIAL, +// 'Minor' => self::PRIO_MINOR, +// 'Major' => self::PRIO_MAJOR, +// 'Critical' => self::PRIO_CRITICAL, +// 'Blocker' => self::PRIO_BLOCKER, +// ]; + + /** + * @var KanbanEntry[]|ArrayCollection + */ + private $inbox; + + /** + * @var KanbanEntry[]|ArrayCollection + */ + private $inProgress; + + /** + * @var KanbanEntry[]|ArrayCollection + */ + private $verification; + + /** + * @var KanbanEntry[]|ArrayCollection + */ + private $done; + + public function __construct() + { + $this->inbox = new ArrayCollection(); + $this->inProgress = new ArrayCollection(); + $this->verification = new ArrayCollection(); + $this->done = new ArrayCollection(); + } + + /** + * @return KanbanEntry[]|ArrayCollection + */ + public function getInbox(): ArrayCollection + { + return $this->inbox; + } + + /** + * @param KanbanEntry[]|ArrayCollection $inbox + * @return KanbanBoard + */ + public function setInbox(ArrayCollection $inbox): KanbanBoard + { + $this->inbox = $inbox; + return $this; + } + + /** + * @param KanbanEntry $inbox + * @return KanbanBoard + */ + public function addInbox(KanbanEntry $inbox): KanbanBoard + { + if (!$this->inbox->contains($inbox)) { + $this->inbox->add($inbox); + } + return $this; + } + + /** + * @param KanbanEntry $inbox + * @return KanbanBoard + */ + public function removeInbox(KanbanEntry $inbox): KanbanBoard + { + if ($this->inbox->contains($inbox)) { + $this->inbox->removeElement($inbox); + } + return $this; + } + + /** + * @return KanbanEntry[]|ArrayCollection + */ + public function getInProgress(): ArrayCollection + { + return $this->inProgress; + } + + /** + * @param KanbanEntry[]|ArrayCollection $inProgress + * @return KanbanBoard + */ + public function setInProgress(ArrayCollection $inProgress): KanbanBoard + { + $this->inProgress = $inProgress; + return $this; + } + + /** + * @param KanbanEntry $inProgress + * @return KanbanBoard + */ + public function addInProgress(KanbanEntry $inProgress): KanbanBoard + { + if (!$this->inProgress->contains($inProgress)) { + $this->inProgress->add($inProgress); + } + return $this; + } + + /** + * @param KanbanEntry $inProgress + * @return KanbanBoard + */ + public function removeInProgress(KanbanEntry $inProgress): KanbanBoard + { + if ($this->inProgress->contains($inProgress)) { + $this->inProgress->removeElement($inProgress); + } + return $this; + } + + /** + * @return KanbanEntry[]|ArrayCollection + */ + public function getVerification(): ArrayCollection + { + return $this->verification; + } + + /** + * @param KanbanEntry[]|ArrayCollection $verification + * @return KanbanBoard + */ + public function setVerification(ArrayCollection $verification): KanbanBoard + { + $this->verification = $verification; + return $this; + } + + /** + * @param KanbanEntry $verification + * @return KanbanBoard + */ + public function addVerification(KanbanEntry $verification): KanbanBoard + { + if (!$this->verification->contains($verification)) { + $this->verification->add($verification); + } + return $this; + } + + /** + * @param KanbanEntry $verification + * @return KanbanBoard + */ + public function removeVerification(KanbanEntry $verification): KanbanBoard + { + if ($this->verification->contains($verification)) { + $this->verification->removeElement($verification); + } + return $this; + } + + /** + * @return KanbanEntry[]|ArrayCollection + */ + public function getDone(): ArrayCollection + { + return $this->done; + } + + /** + * @param KanbanEntry[]|ArrayCollection $verification + * @return KanbanBoard + */ + public function setDone(ArrayCollection $verification): KanbanBoard + { + $this->done = $verification; + return $this; + } + + /** + * @param KanbanEntry $verification + * @return KanbanBoard + */ + public function addDone(KanbanEntry $verification): KanbanBoard + { + if (!$this->done->contains($verification)) { + $this->done->add($verification); + } + return $this; + } + + /** + * @param KanbanEntry $verification + * @return KanbanBoard + */ + public function removeDone(KanbanEntry $verification): KanbanBoard + { + if ($this->done->contains($verification)) { + $this->done->removeElement($verification); + } + return $this; + } + + /** + * @param KanbanEntry[] $toSort + * @return KanbanEntry[] + */ + private function prioSort(array $toSort): array + { + usort($toSort, function (KanbanEntry $a, KanbanEntry $b) { + return $a->getTaurusPrio() <=> $b->getTaurusPrio(); + }); + return $toSort; + } + + /** + * @param KanbanEntry[] $toSort + * @return KanbanEntry[] + */ + private function updatedAtReverseSort(array $toSort): array + { + $toSort = array_filter($toSort, function (KanbanEntry $item) { + return $item->getAssignee() != null && empty($item->getFixVersions()); + }); + usort($toSort, function (KanbanEntry $a, KanbanEntry $b) { + return $b->getUpdatedAt() <=> $a->getUpdatedAt(); + }); + return $toSort; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'inbox' => $this->prioSort($this->inbox->getValues()), + 'inProgress' => $this->prioSort($this->inProgress->getValues()), + 'verification' => $this->prioSort($this->verification->getValues()), + 'done' => $this->updatedAtReverseSort($this->done->getValues()), + ]; + } +} diff --git a/src/App/Entity/KanbanEntry.php b/src/App/Entity/KanbanEntry.php new file mode 100644 index 0000000..29fff20 --- /dev/null +++ b/src/App/Entity/KanbanEntry.php @@ -0,0 +1,689 @@ +functionalAreas = new ArrayCollection(); + $this->additionalAssignees = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @param int $id + * @return KanbanEntry + */ + public function setId(int $id): KanbanEntry + { + $this->id = $id; + return $this; + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @param string $key + * @return KanbanEntry + */ + public function setKey(string $key): KanbanEntry + { + $this->key = $key; + return $this; + } + + /** + * @return string + */ + public function getSummary(): string + { + return $this->summary; + } + + /** + * @param string $summary + * @return KanbanEntry + */ + public function setSummary(string $summary): KanbanEntry + { + $this->summary = $summary; + return $this; + } + + /** + * @return JiraIssueType + */ + public function getIssueType(): ?JiraIssueType + { + return $this->issueType; + } + + /** + * @param JiraIssueType $issueType + * @return KanbanEntry + */ + public function setIssueType(?JiraIssueType $issueType): KanbanEntry + { + $this->issueType = $issueType; + return $this; + } + + /** + * @return JiraStatus + */ + public function getStatus(): ?JiraStatus + { + return $this->status; + } + + /** + * @param JiraStatus $status + * @return KanbanEntry + */ + public function setStatus(?JiraStatus $status): KanbanEntry + { + $this->status = $status; + return $this; + } + + /** + * @return JiraAssignee + */ + public function getAssignee(): ?JiraAssignee + { + return $this->assignee; + } + + /** + * @param JiraAssignee $assignee + * @return KanbanEntry + */ + public function setAssignee(?JiraAssignee $assignee): KanbanEntry + { + $this->assignee = $assignee; + return $this; + } + + /** + * @return JiraAssignee[]|ArrayCollection + */ + public function getAdditionalAssignees(): ?ArrayCollection + { + return $this->additionalAssignees; + } + + /** + * @param JiraAssignee[]|ArrayCollection $additionalAssignees + * @return KanbanEntry + */ + public function setAdditionalAssignees(?ArrayCollection $additionalAssignees): KanbanEntry + { + $this->additionalAssignees = $additionalAssignees; + return $this; + } + + /** + * @param JiraAssignee $assignee + * @return KanbanEntry + */ + public function addAdditionalAssignee(JiraAssignee $assignee): KanbanEntry + { + if(!$this->additionalAssignees->contains($assignee)) { + $this->additionalAssignees->add($assignee); + } + return $this; + } + + /** + * @param JiraAssignee $assignee + * @return KanbanEntry + */ + public function removeAdditionalAssignee(JiraAssignee $assignee): KanbanEntry + { + if($this->additionalAssignees->contains($assignee)) { + $this->additionalAssignees->removeElement($assignee); + } + return $this; + } + + /** + * @return string + */ + public function getIssuePriority(): string + { + return $this->issuePriority; + } + + /** + * @param string $issuePriority + * @return KanbanEntry + */ + public function setIssuePriority(string $issuePriority): KanbanEntry + { + $this->issuePriority = $issuePriority; + return $this; + } + + /** + * @return string + */ + public function getIssuePriorityIcon(): string + { + return $this->issuePriorityIcon; + } + + /** + * @param string $issuePriorityIcon + * @return KanbanEntry + */ + public function setIssuePriorityIcon(string $issuePriorityIcon): KanbanEntry + { + $this->issuePriorityIcon = $issuePriorityIcon; + return $this; + } + + /** + * @return int + */ + public function getPrio(): ?int + { + return $this->prio; + } + + /** + * @param int $prio + * @return KanbanEntry + */ + public function setPrio(?int $prio) + { + $this->prio = $prio; + return $this; + } + + /** + * @return string[] + */ + public function getLabels(): ?array + { + return $this->labels; + } + + /** + * @param string[] $labels + * @return KanbanEntry + */ + public function setLabels(?array $labels): KanbanEntry + { + $this->labels = $labels; + return $this; + } + + /** + * @return array|null + */ + public function getFixVersions(): ?array + { + return $this->fixVersions; + } + + /** + * @param array|null $fixVersions + * @return KanbanEntry + */ + public function setFixVersions(?array $fixVersions): KanbanEntry + { + $this->fixVersions = $fixVersions; + return $this; + } + + /** + * @return string[]|ArrayCollection + */ + public function getFunctionalAreas(): ?ArrayCollection + { + return $this->functionalAreas; + } + + /** + * @param string[]|ArrayCollection $functionalAreas + * @return KanbanEntry + */ + public function setFunctionalAreas(ArrayCollection $functionalAreas): KanbanEntry + { + $this->functionalAreas = $functionalAreas; + return $this; + } + + /** + * @param string $functionalArea + * @return KanbanEntry + */ + public function addFunctionalArea(string $functionalArea): KanbanEntry + { + if(!$this->functionalAreas->contains($functionalArea)) { + $this->functionalAreas->add($functionalArea); + } + return $this; + } + + /** + * @param string $functionalArea + * @return KanbanEntry + */ + public function removeFunctionalArea(string $functionalArea): KanbanEntry + { + if($this->functionalAreas->contains($functionalArea)) { + $this->functionalAreas->removeElement($functionalArea); + } + return $this; + } + + /** + * @return string + */ + public function getExternalId(): ?string + { + return $this->externalId; + } + + /** + * @param string $externalId + * @return KanbanEntry + */ + public function setExternalId(?string $externalId): KanbanEntry + { + $this->externalId = $externalId; + return $this; + } + + /** + * @return string + */ + public function getExternalLink(): ?string + { + return $this->externalLink; + } + + /** + * @param string $externalLink + * @return KanbanEntry + */ + public function setExternalLink(?string $externalLink): KanbanEntry + { + $this->externalLink = $externalLink; + return $this; + } + + /** + * @return string + */ + public function getProject(): ?string + { + return $this->project; + } + + /** + * @param string $project + * @return KanbanEntry + */ + public function setProject(?string $project): KanbanEntry + { + $this->project = $project; + return $this; + } + + /** + * @return string + */ + public function getMhwebStatus(): ?string + { + return $this->mhwebStatus; + } + + /** + * @param string $mhwebStatus + * @return KanbanEntry + */ + public function setMhwebStatus(?string $mhwebStatus): KanbanEntry + { + $this->mhwebStatus = $mhwebStatus; + return $this; + } + + /** + * @return bool + */ + public function getMhwebHot(): ?bool + { + return $this->mhwebHot; + } + + /** + * @param bool $mhwebHot + * @return KanbanEntry + */ + public function setMhwebHot(?bool $mhwebHot): KanbanEntry + { + $this->mhwebHot = $mhwebHot; + return $this; + } + + /** + * @return bool + */ + public function getMhwebExternal(): ?bool + { + return $this->mhwebExternal; + } + + /** + * @param bool $mhwebExternal + * @return KanbanEntry + */ + public function setMhwebExternal(?bool $mhwebExternal): KanbanEntry + { + $this->mhwebExternal = $mhwebExternal; + return $this; + } + + /** + * @return string + */ + public function getTeam(): ?string + { + return $this->team; + } + + /** + * @param string $team + * @return KanbanEntry + */ + public function setTeam(?string $team): KanbanEntry + { + $this->team = $team; + return $this; + } + + /** + * @return string + */ + public function getAnswerCode(): ?string + { + return $this->answerCode; + } + + /** + * @param string $answerCode + * @return KanbanEntry + */ + public function setAnswerCode(?string $answerCode): KanbanEntry + { + $this->answerCode = $answerCode; + return $this; + } + + /** + * @return int + */ + public function getTaurusPrio(): ?int + { + return $this->taurusPrio; + } + + /** + * @param int $taurusPrio + * @return KanbanEntry + */ + public function setTaurusPrio(?int $taurusPrio): KanbanEntry + { + $this->taurusPrio = $taurusPrio; + return $this; + } + + /** + * @return int + */ + public function getWorklog(): int + { + return $this->worklog; + } + + /** + * @param int $worklog + * @return KanbanEntry + */ + public function setWorklog(int $worklog): KanbanEntry + { + $this->worklog = $worklog; + return $this; + } + + /** + * @return int + */ + public function getDaysBlocked(): int + { + return $this->daysBlocked; + } + + /** + * @param int $daysBlocked + * @return KanbanEntry + */ + public function setDaysBlocked(int $daysBlocked): KanbanEntry + { + $this->daysBlocked = $daysBlocked; + return $this; + } + + /** + * @return \DateTime + */ + public function getUpdatedAt(): ?\DateTime + { + return $this->updatedAt; + } + + /** + * @param \DateTime $updatedAt + * @return KanbanEntry + */ + public function setUpdatedAt(?\DateTime $updatedAt): KanbanEntry + { + $this->updatedAt = $updatedAt; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'id' => $this->getId(), + 'key' => $this->getKey(), + 'summary' => $this->getSummary(), + 'issueType' => $this->getIssueType(), + 'status' => $this->getStatus(), + 'assignee' => $this->getAssignee(), + 'additionalAssignees' => $this->getAdditionalAssignees()->getValues(), + 'issuePriority' => $this->getIssuePriority(), + 'issuePriorityIcon' => $this->getIssuePriorityIcon(), + 'labels' => $this->getLabels(), + 'prio' => $this->getPrio(), + 'functionalArea' => $this->getFunctionalAreas()->getValues(), + 'externalId' => $this->getExternalId(), + 'externalLink' => $this->getExternalLink(), + 'project' => $this->getProject(), + 'mhwebStatus' => $this->getMhwebStatus(), + 'mhwebHot' => $this->getMhwebHot(), + 'mhwebExternal' => $this->getMhwebExternal(), + 'team' => $this->getTeam(), + 'answerCode' => $this->getAnswerCode(), + 'taurusPrio' => $this->getTaurusPrio(), + 'worklog' => $this->getWorklog(), + 'daysBlocked' => $this->getDaysBlocked(), + 'updatedAt' => $this->getUpdatedAt(), + ]; + } +} diff --git a/src/App/Handler/AvatarHandler.php b/src/App/Handler/AvatarHandler.php new file mode 100644 index 0000000..ef38b35 --- /dev/null +++ b/src/App/Handler/AvatarHandler.php @@ -0,0 +1,44 @@ +avatarService = $avatarService; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $signum = $request->getAttribute('signum', false); + try { + $avatarImageData = $this->avatarService->getAvatarImageData($signum); + } catch (\UnexpectedValueException $e) { + return new TextResponse("Avatar not found", 404); + } + return (new TextResponse($avatarImageData, 200, [ + 'content-type' => 'image/png', + ]))->withHeader('Expires', '0') + ->withHeader('Cache-Control', 'must-revalidate'); + } +} diff --git a/src/App/Handler/AvatarHandlerFactory.php b/src/App/Handler/AvatarHandlerFactory.php new file mode 100644 index 0000000..43156d2 --- /dev/null +++ b/src/App/Handler/AvatarHandlerFactory.php @@ -0,0 +1,22 @@ +get(AvatarService::class); + return new AvatarHandler($avatarService); + } +} diff --git a/src/App/Handler/KanbanHandler.php b/src/App/Handler/KanbanHandler.php new file mode 100644 index 0000000..7aacabf --- /dev/null +++ b/src/App/Handler/KanbanHandler.php @@ -0,0 +1,40 @@ +dataCollector = $dataCollectorService; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + * @todo filterId + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $filterId = $request->getAttribute('filterId'); + /** @var KanbanBoard $kanbanResult */ + $kanbanResult = $this->dataCollector->getKanbanBoard($filterId); + return new JsonResponse($kanbanResult); + } +} diff --git a/src/App/Handler/KanbanHandlerFactory.php b/src/App/Handler/KanbanHandlerFactory.php new file mode 100644 index 0000000..435daff --- /dev/null +++ b/src/App/Handler/KanbanHandlerFactory.php @@ -0,0 +1,22 @@ +get(JiraCollectorService::class); + return new KanbanHandler($dataCollectorService); + } +} diff --git a/src/App/Service/AvatarService.php b/src/App/Service/AvatarService.php new file mode 100644 index 0000000..0c2963e --- /dev/null +++ b/src/App/Service/AvatarService.php @@ -0,0 +1,63 @@ +httpClient = $client; + $this->config = $config; + $this->cache = $cache; + } + + /** + * Returns avatar image data as string + * + * @param string $signum + * @return string + */ + public function getAvatarImageData(string $signum): string + { + if (!$this->cache->hasItem($signum)) { + $user = $this->config->get('jira.user'); + $password = $this->config->get('jira.password'); + $jiraAvatarUrl = $this->config->get('url.jiraAvatar'); + + $response = $this->httpClient + ->setAuth($user, $password) + ->setUri(sprintf($jiraAvatarUrl, $signum)) + ->send(); + + if (!$response->isSuccess()) { + throw new \UnexpectedValueException("Missing avatar", 404); + } + + $this->cache->setItem($signum, $response->getBody()); + } + + return $this->cache->getItem($signum); + } + +} diff --git a/src/App/Service/AvatarServiceFactory.php b/src/App/Service/AvatarServiceFactory.php new file mode 100644 index 0000000..6ae4e22 --- /dev/null +++ b/src/App/Service/AvatarServiceFactory.php @@ -0,0 +1,21 @@ +get(Client::class); + $cache = $container->get('service.cache'); + $configArray = $container->get('config'); + $config = new Config($configArray['app.config']); + return new AvatarService($httpClient, $config, $cache); + } +} diff --git a/src/App/Service/CacheServiceFactory.php b/src/App/Service/CacheServiceFactory.php new file mode 100644 index 0000000..51a4401 --- /dev/null +++ b/src/App/Service/CacheServiceFactory.php @@ -0,0 +1,27 @@ +getOptions() + ->setFromArray([ + 'ttl' => 28800, + 'cache_dir' => 'data/cache', + ]); + return $cache; + } +} diff --git a/src/App/Service/HttpClientFactory.php b/src/App/Service/HttpClientFactory.php new file mode 100644 index 0000000..65fcfeb --- /dev/null +++ b/src/App/Service/HttpClientFactory.php @@ -0,0 +1,37 @@ +get('config'); + $config = new Config($configArray['app.config']); + + $httpClient = new Client(); + $httpClient->setAdapter($curlAdapter = new Client\Adapter\Curl()); + $curlAdapter->setOptions([ + 'timeout' => 300, + ]); + $curlAdapter + ->setCurlOption(CURLOPT_SSL_VERIFYPEER, false) + ->setCurlOption(CURLOPT_SSL_VERIFYHOST, false); + if ($config->get('http.proxy.enabled', false)) { + $curlAdapter + ->setCurlOption(CURLOPT_PROXYTYPE, $config->get('http.proxy.type')) + ->setCurlOption(CURLOPT_PROXY, $config->get('http.proxy.url')); + } + return $httpClient; + } +} diff --git a/src/App/Service/JiraCollectorService.php b/src/App/Service/JiraCollectorService.php new file mode 100644 index 0000000..85188d3 --- /dev/null +++ b/src/App/Service/JiraCollectorService.php @@ -0,0 +1,245 @@ +cache = $cache; + $this->router = $router; + $this->httpClient = $client; + $this->config = $config; + } + + /** + * @param int $filterId + * @param bool $forceReload + * @return KanbanBoard + */ + public function getKanbanBoard(int $filterId = null, bool $forceReload = false): KanbanBoard + { + $kanbanBoard = $this->cache->getItem('kanbanBoard'); + if ($forceReload || null === $kanbanBoard) { + $user = $this->config->get('jira.user'); + $password = $this->config->get('jira.password'); + /** @var Config $kanbanBoardUriParams */ + $kanbanBoardUriParams = $this->config->get('url.jiraKanbanBoard'); + + $kanbanBoardUri = sprintf( + $kanbanBoardUriParams['baseUrl'], + isset($filterId) ? $filterId : $kanbanBoardUriParams['filterId'], + implode(",", $kanbanBoardUriParams['fields']->toArray()) + ); + + $response = $this->httpClient + ->setUri($kanbanBoardUri) + ->setAuth($user, $password) + ->send(); + + if (!$response->isSuccess()) { + throw new \UnexpectedValueException("Bad JIRA result", $response->getStatusCode()); + } + + $parsedJsonData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY); + + $kanbanBoard = $this->hydrateKanbanBoard($parsedJsonData); + $this->cache->setItem(self::CACHE_KEY_KANBANBOARD, serialize($kanbanBoard)); + } else { + $kanbanBoard = unserialize($kanbanBoard); + } + + return $kanbanBoard; + } + + /** + * @param $parsedJsonData + * @return KanbanBoard + * @todo check if avatar has to be locally cached + */ + private function hydrateKanbanBoard($parsedJsonData): KanbanBoard + { + $kanbanBoard = new KanbanBoard(); + + foreach ($parsedJsonData['issues'] as $jsonIssue) { + $kanbanEntry = new KanbanEntry(); + $kanbanEntry->setId(intval($jsonIssue['id'])) + ->setKey($jsonIssue['key']) + ->setSummary($jsonIssue['fields']['summary']) + ->setExternalLink($jsonIssue['fields']['customfield_10850']) + ->setMhwebStatus($jsonIssue['fields']['customfield_10844']) + ->setAnswerCode($jsonIssue['fields']['customfield_11692']) + ->setIssuePriority($jsonIssue['fields']['priority']['name']) + ->setIssuePriorityIcon($jsonIssue['fields']['priority']['iconUrl']) + ->setLabels($jsonIssue['fields']['labels']) + ->setFixVersions($jsonIssue['fields']['fixVersions']); + + $spikeTimeSpent = 0; + array_map(function ($worklog) use (&$spikeTimeSpent) { + $spikeTimeSpent += strtoupper($worklog['comment']) == 'BLOCKED' + ? 0 + : $worklog['timeSpentSeconds']; + }, $jsonIssue['fields']['worklog']['worklogs']); + $kanbanEntry->setWorklog((int)ceil($spikeTimeSpent / 3600)); + + $secondsBlocked = 0; + array_map(function ($worklog) use (&$secondsBlocked) { + $secondsBlocked += strtoupper($worklog['comment']) == 'BLOCKED' + ? $worklog['timeSpentSeconds'] + : 0; + }, $jsonIssue['fields']['worklog']['worklogs']); + $kanbanEntry->setDaysBlocked((int)ceil($secondsBlocked / 28800)); + + // additional assignees : customfield_10401 + if (isset($jsonIssue['fields']['customfield_10401'])) { + foreach ($jsonIssue['fields']['customfield_10401'] as $assignee) { + $avatarUrl = $this->router->generateUri('avatar.image', [ + 'signum' => $assignee['key'], + ]); + + $jiraAssignee = new JiraAssignee(); + $jiraAssignee->setName($assignee['displayName']) + ->setSignum($assignee['key']) + ->setEmail(strtolower($assignee['emailAddress'])) + ->setAvatar($avatarUrl) + ->setActive($assignee['active']); + $kanbanEntry->addAdditionalAssignee($jiraAssignee); + } + } + + // externalId : customfield_10010 + if (isset($jsonIssue['fields']['customfield_10010'])) { + $kanbanEntry->setExternalId($jsonIssue['fields']['customfield_10010']); + } + + // prio : customfield_10840 + if (isset($jsonIssue['fields']['customfield_11226'])) { + $kanbanEntry->setPrio($jsonIssue['fields']['customfield_11226']); + } + + // functional area : customfield_11225 + if (isset($jsonIssue['fields']['customfield_11225'])) { + foreach ($jsonIssue['fields']['customfield_11225'] as $functionalArea) { + $kanbanEntry->addFunctionalArea($functionalArea['value']); + } + } + + // project : customfield_10840 + if (isset($jsonIssue['fields']['customfield_10840'])) { + $kanbanEntry->setProject($jsonIssue['fields']['customfield_10840']['value']); + } + + // mhweb hot : customfield_10847 + if (isset($jsonIssue['fields']['customfield_10847'])) { + $boolVal = $jsonIssue['fields']['customfield_10847'][0]['value'] == 'yes'; + $kanbanEntry->setMhwebHot($boolVal); + } + + // mhweb external : customfield_10849 + if (isset($jsonIssue['fields']['customfield_10849'])) { + $boolVal = $jsonIssue['fields']['customfield_10849'][0]['value'] == 'yes'; + $kanbanEntry->setMhwebExternal($boolVal); + } + + // team : customfield_10904 + if (isset($jsonIssue['fields']['customfield_10904'])) { + $kanbanEntry->setTeam($jsonIssue['fields']['customfield_10904']['value']); + } + + // team : customfield_12500 + if (isset($jsonIssue['fields']['customfield_12500'])) { + $kanbanEntry->setTaurusPrio($jsonIssue['fields']['customfield_12500']); + } + + // jira status + $jiraStatus = new JiraStatus(); + $jiraStatus->setName($jsonIssue['fields']['status']['name']) + ->setColor($jsonIssue['fields']['status']['statusCategory']['colorName']); + $kanbanEntry->setStatus($jiraStatus); + + // assignee + if ($jsonIssue['fields']['assignee']) { + $avatarUrl = $this->router->generateUri('avatar.image', [ + 'signum' => $jsonIssue['fields']['assignee']['key'], + ]); + + $jiraAssignee = new JiraAssignee(); + $jiraAssignee->setName($jsonIssue['fields']['assignee']['displayName']) + ->setSignum($jsonIssue['fields']['assignee']['key']) + ->setEmail(strtolower($jsonIssue['fields']['assignee']['emailAddress'])) + ->setAvatar($avatarUrl) + ->setActive($jsonIssue['fields']['assignee']['active']); + + $kanbanEntry->setAssignee($jiraAssignee); + unset($jiraAssignee); + } + + // issue type + if ($jsonIssue['fields']['issuetype']) { + $jiraIssueType = new JiraIssueType(); + $jiraIssueType->setName($jsonIssue['fields']['issuetype']['name']) + ->setDescription($jsonIssue['fields']['issuetype']['description']) + ->setIcon($jsonIssue['fields']['issuetype']['iconUrl']); + + $kanbanEntry->setIssueType($jiraIssueType); + unset($jiraIssueType); + } + + $kanbanEntry->setUpdatedAt(new \DateTime($jsonIssue['fields']['updated'])); + + switch ($jiraStatus->getName()) { + case "Backlog": + $kanbanBoard->addInbox($kanbanEntry); + break; + case "In Progress": + $kanbanBoard->addInProgress($kanbanEntry); + break; + case "Verification": + $kanbanBoard->addVerification($kanbanEntry); + break; + case "Done": + $kanbanBoard->addDone($kanbanEntry); + break; + } + unset($kanbanEntry); + } + + return $kanbanBoard; + } +} diff --git a/src/App/Service/JiraCollectorServiceFactory.php b/src/App/Service/JiraCollectorServiceFactory.php new file mode 100644 index 0000000..67ee946 --- /dev/null +++ b/src/App/Service/JiraCollectorServiceFactory.php @@ -0,0 +1,23 @@ +get('service.cache'); + $configArray = $container->get('config'); + $httpClient = $container->get(Client::class); + $config = new Config($configArray['app.config']); + $router = $container->get(RouterInterface::class); + return new JiraCollectorService($cache,$httpClient, $config, $router); + } +}