diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist index 6599af9..691d619 100755 --- a/config/autoload/local.php.dist +++ b/config/autoload/local.php.dist @@ -15,6 +15,7 @@ return [ 'url.jiraAvatar' => 'https://cc-jira.rnd.ki.sw.ericsson.se/secure/useravatar?ownerId=%s', 'url.jiraIssue' => 'https://cc-jira.rnd.ki.sw.ericsson.se/rest/api/2/issue/%s?fields=%s', + 'url.jiraWatchedIssues' => 'https://cc-jira.rnd.ki.sw.ericsson.se/rest/api/2/search?jql=%s&maxResults=1000&fields=%s', 'url.jiraKanbanBoard' => 'https://cc-jira.rnd.ki.sw.ericsson.se/rest/api/2/search?jql=filter=%s&maxResults=1000&fields=%s', 'jira.filterFields' => [ 'summary', diff --git a/config/routes.php b/config/routes.php index fb116dd..a8fae9a 100755 --- a/config/routes.php +++ b/config/routes.php @@ -47,4 +47,5 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac $app->get('/avatars/{signum}', App\Handler\AvatarHandler::class,'avatar.image'); $app->get('/api/kanban/{teamId:\d+}', App\Handler\KanbanHandler::class,'api.team.kanban'); + $app->get('/api/watched/{teamId:\d+}', App\Handler\WatchedHandler::class,'api.team.watched'); }; diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php old mode 100644 new mode 100755 index add1920..3cb7e51 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -42,6 +42,7 @@ class ConfigProvider Handler\SlidePositionHandler::class => Handler\SlidePositionHandlerFactory::class, Handler\AvatarHandler::class => Handler\AvatarHandlerFactory::class, Handler\KanbanHandler::class =>Handler\KanbanHandlerFactory::class, + Handler\WatchedHandler::class =>Handler\WatchedHandlerFactory::class, Service\TeamService::class => Service\TeamServiceFactory::class, Service\SlideManager::class => Service\SlideManagerFactory::class, diff --git a/src/App/Entity/KanbanBoard.php b/src/App/Entity/KanbanBoard.php old mode 100644 new mode 100755 index 1d717d4..1a54b9b --- a/src/App/Entity/KanbanBoard.php +++ b/src/App/Entity/KanbanBoard.php @@ -8,20 +8,6 @@ use Doctrine\Common\Collections\ArrayCollection; class KanbanBoard implements \JsonSerializable { -// const PRIO_TRIVIAL = 0; -// const PRIO_MINOR = 1; -// const PRIO_MAJOR = 2; -// const PRIO_CRITICAL = 3; -// const PRIO_BLOCKER = 4; - -// private $priorityMap = [ -// 'Trivial' => self::PRIO_TRIVIAL, -// 'Minor' => self::PRIO_MINOR, -// 'Major' => self::PRIO_MAJOR, -// 'Critical' => self::PRIO_CRITICAL, -// 'Blocker' => self::PRIO_BLOCKER, -// ]; - /** * @var KanbanEntry[]|ArrayCollection */ diff --git a/src/App/Entity/WatchedIssue.php b/src/App/Entity/WatchedIssue.php new file mode 100755 index 0000000..bad648c --- /dev/null +++ b/src/App/Entity/WatchedIssue.php @@ -0,0 +1,105 @@ +issue; + } + + /** + * @param string $issue + * @return WatchedIssue + */ + public function setIssue(string $issue): WatchedIssue + { + $this->issue = $issue; + return $this; + } + + /** + * @return string + */ + public function getSummary(): ?string + { + return $this->summary; + } + + /** + * @param string $summary + * @return WatchedIssue + */ + public function setSummary(string $summary): WatchedIssue + { + $this->summary = $summary; + return $this; + } + + /** + * @return string + */ + public function getAssignee(): ?string + { + return $this->assignee; + } + + /** + * @param string $assignee + * @return WatchedIssue + */ + public function setAssignee(string $assignee): WatchedIssue + { + $this->assignee = $assignee; + return $this; + } + + /** + * @return WatchedIssueComment + */ + public function getComment(): ?WatchedIssueComment + { + return $this->comment; + } + + /** + * @param WatchedIssueComment $comment + * @return WatchedIssue + */ + public function setComment(WatchedIssueComment $comment): WatchedIssue + { + $this->comment = $comment; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'issue' => $this->getIssue(), + 'summary' => $this->getSummary(), + 'assignee' => $this->getAssignee(), + 'comment' => $this->getComment() ?? new WatchedIssueComment(), + ]; + } +} diff --git a/src/App/Entity/WatchedIssueComment.php b/src/App/Entity/WatchedIssueComment.php new file mode 100755 index 0000000..f562c68 --- /dev/null +++ b/src/App/Entity/WatchedIssueComment.php @@ -0,0 +1,105 @@ +signum; + } + + /** + * @param string $signum + * @return WatchedIssueComment + */ + public function setSignum(string $signum): WatchedIssueComment + { + $this->signum = $signum; + return $this; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param string $name + * @return WatchedIssueComment + */ + public function setName(string $name): WatchedIssueComment + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getContent(): ?string + { + return $this->content; + } + + /** + * @param string $content + * @return WatchedIssueComment + */ + public function setContent(string $content): WatchedIssueComment + { + $this->content = $content; + return $this; + } + + /** + * @return \DateTimeImmutable + */ + public function getDate(): ?\DateTimeImmutable + { + return $this->date; + } + + /** + * @param \DateTimeImmutable $date + * @return WatchedIssueComment + */ + public function setDate(\DateTimeImmutable $date): WatchedIssueComment + { + $this->date = $date; + return $this; + } + + /** + * @return array + */ + function jsonSerialize() + { + return [ + 'signum' => $this->getSignum(), + 'name' => $this->getName(), + 'content' => $this->getContent(), + 'date' => $this->getDate() ? $this->getDate()->format("Y-m-d H:i:s") : null, + ]; + } +} diff --git a/src/App/Handler/WatchedHandler.php b/src/App/Handler/WatchedHandler.php new file mode 100755 index 0000000..4d9f33d --- /dev/null +++ b/src/App/Handler/WatchedHandler.php @@ -0,0 +1,40 @@ +dataCollector = $dataCollectorService; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + * @todo filterId + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $teamId = (int)$request->getAttribute('teamId'); + /** @var KanbanBoard $kanbanResult */ + $kanbanResult = $this->dataCollector->getTeamWatchedIssues($teamId); + return new JsonResponse($kanbanResult); + } +} diff --git a/src/App/Handler/WatchedHandlerFactory.php b/src/App/Handler/WatchedHandlerFactory.php new file mode 100755 index 0000000..d3f3537 --- /dev/null +++ b/src/App/Handler/WatchedHandlerFactory.php @@ -0,0 +1,22 @@ +get(JiraCollectorService::class); + return new WatchedHandler($dataCollectorService); + } +} diff --git a/src/App/Service/JiraCollectorService.php b/src/App/Service/JiraCollectorService.php index aa4977e..6e19814 100755 --- a/src/App/Service/JiraCollectorService.php +++ b/src/App/Service/JiraCollectorService.php @@ -10,6 +10,8 @@ use App\Entity\JiraStatus; use App\Entity\KanbanBoard; use App\Entity\KanbanEntry; use App\Entity\Team; +use App\Entity\WatchedIssue; +use App\Entity\WatchedIssueComment; use Zend\Cache\Storage\StorageInterface; use Zend\Config\Config; use Zend\Expressive\Router\RouterInterface; @@ -25,6 +27,20 @@ class JiraCollectorService const EPIC_TICKET_LINK = 'customfield_11711'; const EPIC_NAME_FIELD = 'customfield_11712'; + const STATUS_DONE = 'done'; + const STATUS_CLOSED = 'closed'; + const STATUS_ANSWERED = 'answered'; + const STATUS_RESOLVED = 'resolved'; + + const IGNORED_STATUSES = [ + self::STATUS_DONE, + self::STATUS_CLOSED, + self::STATUS_ANSWERED, + self::STATUS_RESOLVED, + ]; + + const WATCH_FILTER = 'status NOT IN (%s) AND watcher in (%s) AND "Last change" not in (%s) ORDER BY "Last Comment"'; + /** @var StorageInterface */ private $cache; @@ -110,6 +126,93 @@ class JiraCollectorService return $kanbanBoard; } + /** + * @param int $teamId + * @return array + * @throws \Exception + */ + public function getTeamWatchedIssues(int $teamId) + { + $team = $this->teamService->getTeam($teamId); + $members = array_map(function(array $member): string { + return $member['signum']; + }, $team->getMembers()); + $preparedMembers = sprintf('"%s"', implode('","', $members)); + $filter = sprintf( + self::WATCH_FILTER, + sprintf('"%s"', implode('","', self::IGNORED_STATUSES)), + $preparedMembers, $preparedMembers + ); + $user = $this->config->get('jira.user'); + $password = $this->config->get('jira.password'); + /** @var Config $kanbanBoardUriParams */ + $jiraWatchedIssues = $this->config->get('url.jiraWatchedIssues'); + $kanbanBoardFilterFields = [ + 'assignee', + 'summary', + 'comment', + ]; + $issueFields = implode(",", $kanbanBoardFilterFields); + $jiraIssueUri = sprintf($jiraWatchedIssues, $filter, $issueFields); + + $response = $this->httpClient + ->setUri($jiraIssueUri) + ->setAuth($user, $password) + ->send(); + + if (!$response->isSuccess()) { + throw new \UnexpectedValueException(sprintf( + "Bad JIRA result for URL:\n%s", + $jiraIssueUri + ), $response->getStatusCode()); + } + + return $this->hydrateWatchedIssues(Decoder::decode($response->getBody(), Json::TYPE_ARRAY), $members); + } + + /** + * @param array $parsedJson + * @param array $members + * @return array + * @throws \Exception + */ + private function hydrateWatchedIssues(array $parsedJson, array $members) + { + /** @var WatchedIssue[] $hydratedResult */ + $hydratedResult = []; + + foreach ($parsedJson['issues'] as $issueJson) { + $issueItem = new WatchedIssue(); + $issueItem->setIssue($issueJson['key']) + ->setAssignee($issueJson['fields']['assignee']['name']) + ->setSummary(html_entity_decode($issueJson['fields']['summary'])); + $issueComments = []; + foreach ($issueJson['fields']['comment']['comments'] as $commentJson) { + $issueComment = new WatchedIssueComment(); + $issueComment->setSignum($commentJson['updateAuthor']['name']) + ->setName($commentJson['updateAuthor']['displayName']) + ->setContent(html_entity_decode($commentJson['body'])) + ->setDate(new \DateTimeImmutable($commentJson['updated'])); + $issueComments[] = $issueComment; + } + usort($issueComments, function(WatchedIssueComment $a, WatchedIssueComment $b) { + return $a->getDate() <=> $b->getDate(); + }); + $lastComment = array_pop($issueComments); + unset($issueComments); + $issueItem->setComment($lastComment); + $hydratedResult[] = $issueItem; + } + + /** + * sanity check, we only want items where last change was a comment, but that is not possible + * with JIRA jql at the moment. + */ + return array_filter($hydratedResult, function(WatchedIssue $issue) use ($members) { + return !in_array($issue->getComment()->getSignum(), $members); + }); + } + /** * @param string $parentKey * @return null|string