Compare commits

...

16 Commits

Author SHA1 Message Date
Dávid Danyi
9e5f4c7f68 * things 2019-06-20 13:16:49 +02:00
Dávid Danyi
d27ff7d574 * mtas prod deploy 2018-10-04 15:42:58 +02:00
Dávid Danyi
15455f648e * handle possible null values
* only update caches for enabled services
2018-09-17 17:24:46 +02:00
Dávid Danyi
66bc94037d * builtin slide switching added to the backend 2018-09-17 15:56:30 +02:00
Dávid Danyi
25ff60b34b * add locking to the cli job so it only starts once 2018-09-13 13:10:00 +02:00
Dávid Danyi
c7a2e68a82 * watcher cache filename fixed 2018-09-13 12:53:27 +02:00
Dávid Danyi
bb937a664b * added caching to watchers 2018-09-13 11:33:06 +02:00
Dávid Danyi
3d42f16c38 * watcher api endpoint implemented 2018-09-12 17:20:15 +02:00
Dávid Danyi
9d3fa5fa9d * fixed the issue with cli command, so it only updates jira cache, where there is an id 2018-09-11 17:57:56 +02:00
Dávid Danyi
8c2b7fe548 * added url to jira exception 2018-09-11 16:56:13 +02:00
Dávid Danyi
176cbd86f0 * dailystandup slide-lock function added
* doctrine hydrator immutable date types added
2018-09-11 16:01:14 +02:00
Dávid Danyi
261c43086a * removed pointless avatars 2018-09-07 15:11:02 +02:00
Dávid Danyi
65098ee587 * avatar content-type fix
* team default empty values are empty array
* page caches only updated if there is actually a filter id set for the team
2018-09-06 16:37:37 +02:00
Dávid Danyi
9563eae0b1 * team labels added
* cli task added
2018-09-06 15:38:03 +02:00
Dávid Danyi
cfc388aa77 * multiple team kanban board implementation added
* active flag is now working as intended
* iframe slide type added
* team-slide connection is now many-to-many
2018-09-05 17:01:40 +02:00
Dávid Danyi
c096510b3d * kanban column added for team column config data
* team contains jira filter id and kanban column config
2018-04-27 18:41:03 +02:00
29 changed files with 1606 additions and 159 deletions

View File

@ -1,8 +1,8 @@
{
"name": "zendframework/zend-expressive-skeleton",
"description": "Zend expressive skeleton. Begin developing PSR-15 middleware applications in seconds!",
"name": "mtas/mtas-tv-backend",
"description": "MTAS-TV backend api",
"type": "project",
"homepage": "https://github.com/zendframework/zend-expressive-skeleton",
"homepage": "https://gogs.ragnarok.yvan.hu/MTAS/mtas-tv-backend",
"license": "BSD-3-Clause",
"keywords": [
"skeleton",
@ -63,7 +63,8 @@
"zendframework/zend-json": "^3.1",
"zendframework/zend-log": "^2.10",
"zendframework/zend-servicemanager": "^3.3",
"zendframework/zend-stdlib": "^3.1"
"zendframework/zend-stdlib": "^3.1",
"ext-posix": "*"
},
"require-dev": {
"phpunit/phpunit": "^7.0.1",

16
config/autoload/cli.global.php Executable file
View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'dependencies' => [
'factories' => [
App\Command\UpdatePageCachesCommand::class => App\Command\UpdatePageCachesFactory::class,
],
],
'console' => [
'commands' => [
App\Command\UpdatePageCachesCommand::class,
],
],
];

28
config/autoload/local.php.dist Normal file → Executable file
View File

@ -9,4 +9,32 @@
declare(strict_types=1);
return [
'app.config' => [
'jira.user' => '',
'jira.password' => '',
'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',
'priority',
'issuetype',
'labels',
'assignee',
'status',
'worklog',
'updated',
'fixVersions',
'customfield_11711', // epic link field
'customfield_11712', // epic name
'customfield_13662', // additional jiraAssignees
],
'http.proxy.enabled' => false,
'http.proxy.type' => CURLPROXY_SOCKS5,
'http.proxy.url' => "localhost:1080",
],
];

4
config/routes.php Normal file → Executable file
View File

@ -46,6 +46,6 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac
$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');
$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');
};

79
deploy-local-build.php Normal file
View File

@ -0,0 +1,79 @@
<?php
namespace Deployer;
require 'recipe/common.php';
// Configuration
set('ssh_type', 'native');
set('ssh_multiplexing', true);
#set('repository', 'https://gogs.ragnarok.yvan.hu/MTAS/mtas-tv-backend.git');
set('shared_files', [
'config/autoload/local.php',
'config/autoload/los-basepath.local.php',
'config/autoload/doctrine.local.php',
'data/production.db',
]);
/*
set('shared_dirs', [
'data/persistent',
]);
*/
set('writable_dirs', [
'data/cache',
'data/logs',
]);
set('keep_releases', 3);
set('default_stage', 'production');
// Servers - mtas : esekivws5222a.rnd.ki.sw.ericsson.se
host('mtas')
->stage('production')
->user('edvidan')
->forwardAgent()
->set('deploy_path', '/proj/webdocs/mtoolbox/root/mtastv-inst/backend');
task('build', function() {
run('composer install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');
run('composer archive --file=release-pkg');
run('tar -czf vendor-pkg.tar.gz vendor');
})->local();
task('upload', function() {
upload('release-pkg.tar', '{{release_path}}');
upload('vendor-pkg.tar.gz', '{{release_path}}');
cd('{{release_path}}');
run('tar -xf release-pkg.tar');
run('tar -xzf vendor-pkg.tar.gz');
});
task('clean-uploaded', function() {
cd('{{release_path}}');
run('rm -f release-pkg.tar vendor-pkg.tar.gz');
});
task('restore-dev', function() {
run('composer install --verbose --no-progress --no-interaction');
})->local();
task('release', [
'deploy:lock',
'deploy:prepare',
'deploy:release',
'upload',
'clean-uploaded',
'deploy:shared',
'deploy:writable',
'deploy:symlink',
'deploy:unlock',
]);
desc('Deploy your project');
task('deploy', [
'build',
'release',
'cleanup',
'restore-dev',
'success',
]);

View File

@ -23,9 +23,8 @@ set('keep_releases', 3);
set('default_stage', 'production');
// Servers
host('vasgyuro.tsp')
->stage('production')
->stage('staging')
->user('edvidan')
->forwardAgent()
->set('php_service_name', 'php7.1-fpm')
@ -37,7 +36,7 @@ task('php-fpm:reload', function () {
// The user must have rights for restart service
// /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service
run('sudo service {{php_service_name}} reload');
}); //->onlyOn('alfheim');
});
after('deploy:symlink', 'php-fpm:reload');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 B

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Service\JiraCollectorService;
use App\Service\TeamService;
use LosMiddleware\LosLog\StaticLogger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpdatePageCachesCommand extends Command
{
const LOCK_FILE = 'data/update-caches-cron.lock';
/** @var JiraCollectorService */
private $jiraCollectorService;
/** @var TeamService */
private $teamService;
public function __construct(JiraCollectorService $jiraCollectorService,
TeamService $teamService)
{
$this->jiraCollectorService = $jiraCollectorService;
$this->teamService = $teamService;
parent::__construct();
}
/**
* Configure the command
*/
protected function configure()
{
$this->setName('cache:update')
->setDescription('Updates page-cache data for kanban pages');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null|void
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($this->isLocked()) {
$output->writeln("Lock file exists, not starting.");
return;
}
$this->createLock();
$teams = $this->teamService->listTeams();
foreach ($teams as $team) {
if ($team->isKanbanEnabled() && null !== $team->getFilterId()) {
set_time_limit(30);
$this->jiraCollectorService->getKanbanBoard($team->getId(), true);
}
if ($team->isWatchedEnabled()) {
set_time_limit(30);
$this->jiraCollectorService->getTeamWatchedIssues($team->getId(), true);
}
}
$this->releaseLock();
}
/**
* Create the lock file
*/
private function createLock(): void
{
file_put_contents(self::LOCK_FILE, posix_getpid());
}
/**
* Remove the lock file
* @return bool
*/
private function releaseLock(): bool
{
return unlink(self::LOCK_FILE);
}
/**
* Check if lock file exists, also removes stale lock
* @return bool
* @throws \LosMiddleware\LosLog\Exception\InvalidArgumentException
*/
private function isLocked(): bool
{
if (!file_exists(self::LOCK_FILE)) {
return false;
}
StaticLogger::save("[1] LOCK file exists.");
$pid = (int)file_get_contents(self::LOCK_FILE);
if (posix_getpgid($pid)) {
StaticLogger::save("[2] PID is running");
$binPath = readlink("/proc/${pid}/exe");
$binName = basename($binPath);
return substr($binName, 0, 3) === 'php';
}
StaticLogger::save("[2] No process found.");
return false;
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Handler\AvatarHandler;
use App\Service\JiraCollectorService;
use App\Service\TeamService;
use Psr\Container\ContainerInterface;
use Zend\Expressive\MiddlewareContainer;
use Zend\Expressive\MiddlewareFactory;
use Zend\Expressive\Router\Route;
use Zend\Expressive\Router\RouterInterface;
class UpdatePageCachesFactory
{
/**
* @param ContainerInterface $container
* @return UpdatePageCachesCommand
*/
public function __invoke(ContainerInterface $container)
{
$middlewareContainer = new MiddlewareContainer($container);
$factory = new MiddlewareFactory($middlewareContainer);
$avatarHandlerMiddleware = $factory->prepare(AvatarHandler::class);
$avatarRoute = new Route(
'/avatars/{signum}',
$avatarHandlerMiddleware,
Route::HTTP_METHOD_ANY,
'avatar.image'
);
/** @var \Zend\Expressive\Router\FastRouteRouter $router */
$router = $container->get(RouterInterface::class);
$router->addRoute($avatarRoute);
$jiraCollectorService = $container->get(JiraCollectorService::class);
$teamService = $container->get(TeamService::class);
return new UpdatePageCachesCommand($jiraCollectorService, $teamService);
}
}

1
src/App/ConfigProvider.php Normal file → Executable file
View File

@ -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,

25
src/App/Entity/KanbanBoard.php Normal file → Executable file
View File

@ -8,19 +8,13 @@ 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,
// ];
const PRIO_MAP = [
'Trivial' => 0,
'Minor' => 1,
'Major' => 2,
'Critical' => 3,
'Blocker' => 4,
];
/**
* @var KanbanEntry[]|ArrayCollection
@ -225,7 +219,10 @@ class KanbanBoard implements \JsonSerializable
private function prioSort(array $toSort): array
{
usort($toSort, function (KanbanEntry $a, KanbanEntry $b) {
return $a->getTaurusPrio() <=> $b->getTaurusPrio();
// if (null !== $a->getTaurusPrio()) {
return $a->getTaurusPrio() <=> $b->getTaurusPrio();
// }
// return self::PRIO_MAP[$b->getIssuePriority()] <=> self::PRIO_MAP[$a->getIssuePriority()];
});
return $toSort;
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Entity;
class KanbanColumn implements \JsonSerializable
{
/** @var string */
private $jiraStatusName = "";
/** @var string */
private $label = "";
/** @var integer */
private $wipLimit = 0;
/**
* @return string
*/
public function getJiraStatusName(): ?string
{
return $this->jiraStatusName;
}
/**
* @param string $jiraStatusName
* @return KanbanColumn
*/
public function setJiraStatusName(?string $jiraStatusName): KanbanColumn
{
$this->jiraStatusName = $jiraStatusName;
return $this;
}
/**
* @return string
*/
public function getLabel(): ?string
{
return $this->label;
}
/**
* @param string $label
* @return KanbanColumn
*/
public function setLabel(?string $label): KanbanColumn
{
$this->label = $label;
return $this;
}
/**
* @return int
*/
public function getWipLimit(): ?int
{
return $this->wipLimit;
}
/**
* @param int $wipLimit
* @return KanbanColumn
*/
public function setWipLimit(?int $wipLimit): KanbanColumn
{
$this->wipLimit = $wipLimit;
return $this;
}
/**
* @return array
*/
function jsonSerialize()
{
return [
'jiraStatusName' => $this->getJiraStatusName(),
'label' => $this->getLabel(),
'wipLimit' => $this->getWipLimit(),
];
}
}

55
src/App/Entity/KanbanEntry.php Normal file → Executable file
View File

@ -29,6 +29,12 @@ class KanbanEntry implements \JsonSerializable
*/
private $issueType;
/**
* JIRA: link to parent is in a custom field, parent has the epic name as a custom field
* @var string
*/
private $epicName;
/**
* @var JiraStatus
*/
@ -40,7 +46,6 @@ class KanbanEntry implements \JsonSerializable
private $assignee;
/**
* JIRA: customfield_10401
* @var JiraAssignee[]
*/
private $additionalAssignees;
@ -66,68 +71,57 @@ class KanbanEntry implements \JsonSerializable
private $fixVersions;
/**
* JIRA: customfield_11226
* @var int
*/
private $prio;
/**
* JIRA: customfield_11225
* @var string[]|ArrayCollection
*/
private $functionalAreas;
/**
* JIRA: customfield_10010
* @var string
*/
private $externalId;
/**
* JIRA: customfield_10850
* @var string
*/
private $externalLink;
/**
* JIRA: customfield_10840
* @var string
*/
private $project;
/**
* JIRA: customfield_10844
* @var string
*/
private $mhwebStatus;
/**
* JIRA: customfield_10847
* @var bool
*/
private $mhwebHot;
/**
* JIRA: customfield_10849
* @var bool
*/
private $mhwebExternal = false;
/**
* JIRA: customfield_10904
* @var string
*/
private $team;
/**
* JIRA: customfield_11692
* @var string
*/
private $answerCode;
/**
* ITS OVER 9000!
* JIRA: customfield_12500
* @var int
*/
private $taurusPrio = 9001;
@ -228,6 +222,24 @@ class KanbanEntry implements \JsonSerializable
return $this;
}
/**
* @return string
*/
public function getEpicName(): ?string
{
return $this->epicName;
}
/**
* @param string $epicName
* @return KanbanEntry
*/
public function setEpicName(?string $epicName): KanbanEntry
{
$this->epicName = $epicName;
return $this;
}
/**
* @return JiraStatus
*/
@ -309,7 +321,7 @@ class KanbanEntry implements \JsonSerializable
/**
* @return string
*/
public function getIssuePriority(): string
public function getIssuePriority(): ?string
{
return $this->issuePriority;
}
@ -318,7 +330,7 @@ class KanbanEntry implements \JsonSerializable
* @param string $issuePriority
* @return KanbanEntry
*/
public function setIssuePriority(string $issuePriority): KanbanEntry
public function setIssuePriority(?string $issuePriority): KanbanEntry
{
$this->issuePriority = $issuePriority;
return $this;
@ -327,7 +339,7 @@ class KanbanEntry implements \JsonSerializable
/**
* @return string
*/
public function getIssuePriorityIcon(): string
public function getIssuePriorityIcon(): ?string
{
return $this->issuePriorityIcon;
}
@ -336,7 +348,7 @@ class KanbanEntry implements \JsonSerializable
* @param string $issuePriorityIcon
* @return KanbanEntry
*/
public function setIssuePriorityIcon(string $issuePriorityIcon): KanbanEntry
public function setIssuePriorityIcon(?string $issuePriorityIcon): KanbanEntry
{
$this->issuePriorityIcon = $issuePriorityIcon;
return $this;
@ -664,22 +676,13 @@ class KanbanEntry implements \JsonSerializable
'key' => $this->getKey(),
'summary' => $this->getSummary(),
'issueType' => $this->getIssueType(),
'epicName' => $this->getEpicName(),
'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(),

153
src/App/Entity/Slide.php Normal file → Executable file
View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use JsonSerializable;
@ -15,6 +17,12 @@ use JsonSerializable;
*/
class Slide implements JsonSerializable
{
const TYPE_MARKDOWN = 'markdown';
const TYPE_IFRAME = 'iframe';
const VISIBILITY_PUBLIC = 'public';
const VISIBILITY_TEAM = 'team';
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
@ -30,18 +38,29 @@ class Slide implements JsonSerializable
private $title;
/**
* @ORM\ManyToOne(targetEntity="Team", inversedBy="slides")
* @ORM\JoinColumn(name="team_id", referencedColumnName="id")
* @var Team
* @ORM\Column(name="type", type="string", length=50, options={"default" = "markdown"})
* @var string
*/
private $team;
private $type = self::TYPE_MARKDOWN;
/**
* @ORM\Column(name="slide_data", type="text", nullable=false)
* @ORM\Column(name="visibility", type="string", length=50, options={"default" = "public"})
* @var string
*/
private $visibility = self::VISIBILITY_PUBLIC;
/**
* @ORM\Column(name="slide_data", type="text", nullable=true)
* @var string
*/
private $slideData;
/**
* @ORM\Column(name="slide_url", type="text", nullable=true)
* @var string
*/
private $slideUrl;
/**
* @ORM\Column(name="is_visible", type="boolean")
* @var bool
@ -69,6 +88,17 @@ class Slide implements JsonSerializable
*/
private $position;
/**
* @ORM\ManyToMany(targetEntity="Team", inversedBy="slides", cascade={"persist", "remove"})
* @var Team[]|ArrayCollection
*/
private $teams;
public function __construct()
{
$this->teams = new ArrayCollection();
}
/**
* @return int
*/
@ -106,20 +136,38 @@ class Slide implements JsonSerializable
}
/**
* @return Team
* @return string
*/
public function getTeam(): ?Team
public function getType(): ?string
{
return $this->team;
return $this->type;
}
/**
* @param Team $team
* @param string $type
* @return Slide
*/
public function setTeam(?Team $team): Slide
public function setType(?string $type): Slide
{
$this->team = $team;
$this->type = $type;
return $this;
}
/**
* @return string
*/
public function getVisibility(): ?string
{
return $this->visibility;
}
/**
* @param string $visibility
* @return Slide
*/
public function setVisibility(?string $visibility): Slide
{
$this->visibility = $visibility;
return $this;
}
@ -135,12 +183,30 @@ class Slide implements JsonSerializable
* @param string $slideData
* @return Slide
*/
public function setSlideData(string $slideData): Slide
public function setSlideData(?string $slideData): Slide
{
$this->slideData = $slideData;
return $this;
}
/**
* @return string
*/
public function getSlideUrl(): ?string
{
return $this->slideUrl;
}
/**
* @param string $slideUrl
* @return Slide
*/
public function setSlideUrl(?string $slideUrl): Slide
{
$this->slideUrl = $slideUrl;
return $this;
}
/**
* @return bool
*/
@ -213,6 +279,64 @@ class Slide implements JsonSerializable
return $this;
}
/**
* @param Team $team
* @return Slide
*/
public function addTeam(Team $team): Slide
{
if (!$this->teams->contains($team)) {
$this->teams->add($team);
}
//$team->addSlide($this);
return $this;
}
/**
* @param Team[] $teams
* @return Slide
*/
public function addTeams($teams): Slide
{
foreach ($teams as $team) {
$this->addTeam($team);
}
return $this;
}
/**
* @return Team[]|Collection
*/
public function getTeams(): Collection
{
return $this->teams;
}
/**
* @param Team $team
* @return Slide
*/
public function removeTeam(Team $team): Slide
{
if ($this->teams->contains($team)) {
$this->teams->removeElement($team);
}
//$team->removeSlide($this);
return $this;
}
/**
* @param Team[] $teams
* @return Slide
*/
public function removeTeams($teams): Slide
{
foreach ($teams as $team) {
$this->removeTeam($team);
}
return $this;
}
/**
* @return array
*/
@ -221,8 +345,11 @@ class Slide implements JsonSerializable
return [
'id' => $this->getId(),
'title' => $this->getTitle(),
'team' => $this->getTeam(),
'type' => $this->getType(),
'visibility' => $this->getVisibility(),
'teams' => $this->getTeams()->getValues(),
'slideData' => $this->getSlideData(),
'slideUrl' => $this->getSlideUrl(),
'isVisible' => $this->isVisible(),
'createdAt' => $this->getCreatedAt()
? $this->getCreatedAt()->format("Y-m-d H:i:s")

334
src/App/Entity/Team.php Normal file → Executable file
View File

@ -37,21 +37,97 @@ class Team implements JsonSerializable
private $members;
/**
* @ORM\OneToMany(
* targetEntity="Slide",
* mappedBy="team",
* cascade={"persist", "remove"},
* orphanRemoval=true
* @ORM\Column(name="labels", type="json", nullable=true)
* @var array
*/
private $labels;
/**
* @ORM\ManyToMany(targetEntity="Slide", mappedBy="teams", cascade={"persist", "remove"})
* @ORM\JoinTable(
* name="team_slides",
* joinColumns={
* @ORM\JoinColumn(name="team_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="slide_id", referencedColumnName="id")
* }
* )
* @var Slide[]|Collection
*/
private $slides;
/**
* @ORM\Column(name="kanban_enabled", type="boolean", options={"default" = true})
* @var bool
*/
private $kanbanEnabled = true;
/**
* @ORM\Column(name="commit_tracker_enabled", type="boolean", options={"default" = true})
* @var bool
*/
private $commitTrackerEnabled = true;
/**
* @ORM\Column(name="watched_enabled", type="boolean", options={"default" = true})
* @var bool
*/
private $watchedEnabled = true;
/**
* @ORM\Column(name="filter_id", type="integer", nullable=true)
* @var int
*/
private $filterId;
/**
* @ORM\Column(name="daily_lock_enabled", type="boolean", options={"default" = false})
* @var bool
*/
private $dailyLockEnabled = false;
/**
* @ORM\Column(name="daily_start_time", type="time_immutable", nullable=true)
* @var \DateTimeImmutable
*/
private $dailyStartTime;
/**
* @ORM\Column(name="daily_end_time", type="time_immutable", nullable=true)
* @var \DateTimeImmutable
*/
private $dailyEndTime;
/**
* @ORM\Column(name="backlog_column", type="json", nullable=true)
* @var KanbanColumn
*/
private $backlogColumn;
/**
* @ORM\Column(name="inprogress_column", type="json", nullable=true)
* @var KanbanColumn
*/
private $inprogressColumn;
/**
* @ORM\Column(name="verification_column", type="json", nullable=true)
* @var KanbanColumn
*/
private $verificationColumn;
/**
* @ORM\Column(name="done_column", type="json", nullable=true)
* @var KanbanColumn
*/
private $doneColumn;
/**
* @ORM\Column(name="is_active", type="boolean")
* @var bool
*/
private $isActive;
private $isActive = true;
/**
* @ORM\Column(name="created_at", type="datetime_immutable", nullable=true)
@ -69,8 +145,14 @@ class Team implements JsonSerializable
public function __construct()
{
$this->slides = new ArrayCollection;
$this->members = new \ArrayObject;
$this->labels = new \ArrayObject;
$this->slides = new ArrayCollection;
$this->backlogColumn = new KanbanColumn();
$this->inprogressColumn = new KanbanColumn();
$this->verificationColumn = new KanbanColumn();
$this->doneColumn = new KanbanColumn();
}
/**
@ -127,15 +209,34 @@ class Team implements JsonSerializable
return $this;
}
/**
* @return array
*/
public function getLabels()
{
return $this->labels;
}
/**
* @param array $labels
* @return Team
*/
public function setLabels(array $labels): Team
{
$this->labels = $labels;
return $this;
}
/**
* @param Slide $slide
* @return Team
*/
public function addSlides(Slide $slide): Team
public function addSlide(Slide $slide): Team
{
if (!$this->slides->contains($slide)) {
$this->slides->removeElement($slide);
}
//$slide->addTeam($this);
return $this;
}
@ -156,6 +257,205 @@ class Team implements JsonSerializable
if ($this->slides->contains($slide)) {
$this->slides->removeElement($slide);
}
//$slide->removeTeam($this);
return $this;
}
/**
* @return bool
*/
public function isKanbanEnabled(): bool
{
return $this->kanbanEnabled;
}
/**
* @param bool $kanbanEnabled
* @return Team
*/
public function setKanbanEnabled(bool $kanbanEnabled): Team
{
$this->kanbanEnabled = $kanbanEnabled;
return $this;
}
/**
* @return bool
*/
public function isCommitTrackerEnabled(): bool
{
return $this->commitTrackerEnabled;
}
/**
* @param bool $commitTrackerEnabled
* @return Team
*/
public function setCommitTrackerEnabled(bool $commitTrackerEnabled): Team
{
$this->commitTrackerEnabled = $commitTrackerEnabled;
return $this;
}
/**
* @return bool
*/
public function isWatchedEnabled(): bool
{
return $this->watchedEnabled;
}
/**
* @param bool $watchedEnabled
* @return Team
*/
public function setWatchedEnabled(bool $watchedEnabled): Team
{
$this->watchedEnabled = $watchedEnabled;
return $this;
}
/**
* @return int
*/
public function getFilterId(): ?int
{
return $this->filterId;
}
/**
* @param int $filterId
* @return Team
*/
public function setFilterId(?int $filterId): Team
{
$this->filterId = $filterId;
return $this;
}
/**
* @return bool
*/
public function isDailyLockEnabled(): bool
{
return $this->dailyLockEnabled;
}
/**
* @param bool $dailyLockEnabled
* @return Team
*/
public function setDailyLockEnabled(bool $dailyLockEnabled): Team
{
$this->dailyLockEnabled = $dailyLockEnabled;
return $this;
}
/**
* @return \DateTimeImmutable
*/
public function getDailyStartTime(): ?\DateTimeImmutable
{
return $this->dailyStartTime;
}
/**
* @param \DateTimeInterface $dailyStartTime
* @return Team
*/
public function setDailyStartTime(?\DateTimeInterface $dailyStartTime): Team
{
$this->dailyStartTime = $dailyStartTime;
return $this;
}
/**
* @return \DateTimeImmutable
*/
public function getDailyEndTime(): ?\DateTimeImmutable
{
return $this->dailyEndTime;
}
/**
* @param \DateTimeInterface $dailyEndTime
* @return Team
*/
public function setDailyEndTime(?\DateTimeInterface $dailyEndTime): Team
{
$this->dailyEndTime = $dailyEndTime;
return $this;
}
/**
* @return array|KanbanColumn
*/
public function getBacklogColumn()
{
return $this->backlogColumn;
}
/**
* @param array $backlogColumn
* @return Team
*/
public function setBacklogColumn(?array $backlogColumn): Team
{
$this->backlogColumn = $backlogColumn;
return $this;
}
/**
* @return array|KanbanColumn
*/
public function getInprogressColumn()
{
return $this->inprogressColumn;
}
/**
* @param array $inprogressColumn
* @return Team
*/
public function setInprogressColumn(array $inprogressColumn): Team
{
$this->inprogressColumn = $inprogressColumn;
return $this;
}
/**
* @return array|KanbanColumn
*/
public function getVerificationColumn()
{
return $this->verificationColumn;
}
/**
* @param array $verificationColumn
* @return Team
*/
public function setVerificationColumn(?array $verificationColumn): Team
{
$this->verificationColumn = $verificationColumn;
return $this;
}
/**
* @return array|KanbanColumn
*/
public function getDoneColumn()
{
return $this->doneColumn;
}
/**
* @param array $doneColumn
* @return Team
*/
public function setDoneColumn(?array $doneColumn): Team
{
$this->doneColumn = $doneColumn;
return $this;
}
@ -221,7 +521,23 @@ class Team implements JsonSerializable
return [
'id' => $this->getId(),
'name' => $this->getName(),
'members' => $this->getMembers(),
'members' => $this->getMembers() ?? [],
'labels' => $this->getLabels() ?? [],
'kanbanEnabled' => $this->isKanbanEnabled(),
'commitTrackerEnabled' => $this->isCommitTrackerEnabled(),
'watchedEnabled' => $this->isWatchedEnabled(),
'filterId' => $this->getFilterId(),
'dailyLockEnabled' => $this->isDailyLockEnabled(),
'dailyStartTime' => $this->getDailyStartTime()
? $this->getDailyStartTime()->format("H:i")
: null,
'dailyEndTime' => $this->getDailyEndTime()
? $this->getDailyEndTime()->format("H:i")
: null,
'backlogColumn' => $this->getBacklogColumn() ?? new KanbanColumn(),
'inprogressColumn' => $this->getInprogressColumn() ?? new KanbanColumn(),
'verificationColumn' => $this->getVerificationColumn() ?? new KanbanColumn(),
'doneColumn' => $this->getDoneColumn() ?? new KanbanColumn(),
'isActive' => $this->isActive(),
'createdAt' => $this->getCreatedAt()
? $this->getCreatedAt()->format("Y-m-d H:i:s")

105
src/App/Entity/WatchedIssue.php Executable file
View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Entity;
class WatchedIssue implements \JsonSerializable
{
/** @var string */
private $issue;
/** @var string */
private $summary;
/** @var string */
private $assignee;
/** @var WatchedIssueComment */
private $comment;
/**
* @return string
*/
public function getIssue(): ?string
{
return $this->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(),
];
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Entity;
class WatchedIssueComment implements \JsonSerializable
{
/** @var string */
private $signum;
/** @var string */
private $name;
/** @var string */
private $content;
/** @var \DateTimeImmutable */
private $date;
/**
* @return string
*/
public function getSignum(): ?string
{
return $this->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,
];
}
}

53
src/App/Form/Slide.php Normal file → Executable file
View File

@ -30,9 +30,34 @@ class Slide
*/
private $title;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\InputFilter("Zend\Filter\StringTrim")
* @Annotation\Options({
* "label": "Slide type"
* })
* @var string
*/
private $type;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\InputFilter("Zend\Filter\StringTrim")
* @Annotation\Options({
* "label": "Slide visibility"
* })
* @var string
*/
private $visibility;
/**
* @Annotation\Type("doctrine.object_select")
* @Annotation\Required(false)
* @Annotation\Attributes({
* "multiple": true
* })
* @Annotation\Options({
* "property": "name",
* "label": "Team",
@ -48,21 +73,39 @@ class Slide
* }
* }
* })
* @var
* @var Team[]
*/
private $team;
private $teams;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\Required(false)
* @Annotation\InputFilter("Zend\Filter\StringTrim")
* @Annotation\Options({
* "label": "Slide contents"
* "label": "Slide contents (type markdown)"
* })
* @var
* @var string
*/
private $slideData;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(false)
* @Annotation\InputFilter("Zend\Filter\StringTrim")
* @Annotation\Options({
* "label": "Slide url (type iframe)"
* })
* @Annotation\Validator({
* "name":"Uri",
* "options": {
* "allowAbsolute": true,
* "allowRelative": false,
* }
* })
* @var string
*/
private $slideUrl;
/**
* @Annotation\Type("Zend\Form\Element\Checkbox")
* @Annotation\Options({

150
src/App/Form/Team.php Normal file → Executable file
View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Form;
use App\Entity\KanbanColumn;
use Zend\Form\Annotation;
/**
@ -41,6 +42,155 @@ class Team
*/
private $members;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(false)
* @Annotation\Options({
* "label": "Labels"
* })
* @var array
*/
private $labels;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Options({
* "label": "Active"
* })
* @Annotation\Validator({
* "name":"NotEmpty",
* "options": {"type": Zend\Validator\NotEmpty::NULL}
* })
* @Annotation\Required(false)
* @var bool
*/
private $kanbanEnabled;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Options({
* "label": "Active"
* })
* @Annotation\Validator({
* "name":"NotEmpty",
* "options": {"type": Zend\Validator\NotEmpty::NULL}
* })
* @Annotation\Required(false)
* @var bool
*/
private $commitTrackerEnabled;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Options({
* "label": "Active"
* })
* @Annotation\Validator({
* "name":"NotEmpty",
* "options": {"type": Zend\Validator\NotEmpty::NULL}
* })
* @Annotation\Required(false)
* @var bool
*/
private $watchedEnabled;
/**
* @Annotation\Type("Zend\Form\Element\Number")
* @Annotation\Required(true)
* @Annotation\Options({
* "label": "Jira filter id"
* })
* @var int
*/
private $filterId;
/**
* Also a dummy field, a
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Options({
* "label": "Enabled"
* })
* @Annotation\Validator({
* "name":"NotEmpty",
* "options": {"type": Zend\Validator\NotEmpty::NULL}
* })
* @Annotation\Required(false)
* @var bool
*/
private $dailyLockEnabled;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(false)
* @Annotation\Options({
* "label": "Start time"
* })
* @var array
*/
private $dailyStartTime;
/**
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(false)
* @Annotation\Options({
* "label": "End time"
* })
* @var array
*/
private $dailyEndTime;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\Options({
* "label": "1st column"
* })
* @var KanbanColumn
*/
private $backlogColumn;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\Options({
* "label": "1st column"
* })
* @var KanbanColumn
*/
private $inprogressColumn;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\Options({
* "label": "1st column"
* })
* @var KanbanColumn
*/
private $verificationColumn;
/**
* This is a dummy field, not a text actually. Only used to filter the input
* @Annotation\Type("Zend\Form\Element\Text")
* @Annotation\Required(true)
* @Annotation\Options({
* "label": "1st column"
* })
* @var KanbanColumn
*/
private $doneColumn;
/**
* Also a dummy field, a
* @Annotation\Type("Zend\Form\Element\Text")

5
src/App/Handler/AvatarHandler.php Normal file → Executable file
View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Handler;
use App\Service\AvatarService;
use finfo;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -36,8 +37,10 @@ class AvatarHandler implements RequestHandlerInterface
} catch (\UnexpectedValueException $e) {
return new TextResponse("Avatar not found", 404);
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$contentType = $finfo->buffer($avatarImageData);
return (new TextResponse($avatarImageData, 200, [
'content-type' => 'image/png',
'content-type' => $contentType,
]))->withHeader('Expires', '0')
->withHeader('Cache-Control', 'must-revalidate');
}

4
src/App/Handler/KanbanHandler.php Normal file → Executable file
View File

@ -32,9 +32,9 @@ class KanbanHandler implements RequestHandlerInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$filterId = $request->getAttribute('filterId');
$teamId = (int)$request->getAttribute('teamId');
/** @var KanbanBoard $kanbanResult */
$kanbanResult = $this->dataCollector->getKanbanBoard($filterId);
$kanbanResult = $this->dataCollector->getKanbanBoard($teamId);
return new JsonResponse($kanbanResult);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Handler;
use App\Entity\KanbanBoard;
use App\Service\JiraCollectorService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\JsonResponse;
class WatchedHandler implements RequestHandlerInterface
{
/** @var JiraCollectorService */
private $dataCollector;
/**
* KanbanAction constructor.
* @param JiraCollectorService $dataCollectorService
*/
public function __construct(JiraCollectorService $dataCollectorService)
{
$this->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);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Handler;
use App\Service\JiraCollectorService;
use Interop\Container\ContainerInterface;
class WatchedHandlerFactory
{
/**
* @param ContainerInterface $container
* @return WatchedHandler
*/
public function __invoke(ContainerInterface $container)
{
/** @var JiraCollectorService $dataCollectorService */
$dataCollectorService = $container->get(JiraCollectorService::class);
return new WatchedHandler($dataCollectorService);
}
}

310
src/App/Service/JiraCollectorService.php Normal file → Executable file
View File

@ -9,6 +9,9 @@ use App\Entity\JiraIssueType;
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;
@ -18,8 +21,26 @@ use Zend\Json\Json;
class JiraCollectorService
{
const CACHE_KEY_KANBANBOARD = 'kanbanBoard';
const CACHE_KEY_WATCHED = 'watchedIssues';
const BACKLOG_FIELD_DELIMITER = ';';
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;
@ -33,39 +54,55 @@ class JiraCollectorService
/** @var RouterInterface */
private $router;
/** @var TeamService */
private $teamService;
/** @var array */
private $cachedEpics = [];
/**
* JiraClientService constructor.
* @param StorageInterface $cache
* @param Client $client
* @param Config $config
* @param RouterInterface $router
* @param TeamService $teamService
*/
public function __construct(StorageInterface $cache, Client $client, Config $config, RouterInterface $router)
public function __construct(StorageInterface $cache,
Client $client,
Config $config,
RouterInterface $router,
TeamService $teamService)
{
$this->cache = $cache;
$this->router = $router;
$this->httpClient = $client;
$this->config = $config;
$this->teamService = $teamService;
}
/**
* @param int $filterId
* @param int $teamId
* @param bool $forceReload
* @return KanbanBoard
*/
public function getKanbanBoard(int $filterId = null, bool $forceReload = false): KanbanBoard
public function getKanbanBoard(int $teamId, bool $forceReload = false): KanbanBoard
{
$kanbanBoard = $this->cache->getItem('kanbanBoard');
$team = $this->teamService->getTeam($teamId);
$teamName = $team->getName();
$kanbanBoard = $this->cache->getItem(sprintf("%s-%s", self::CACHE_KEY_KANBANBOARD, $teamName));
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 = $this->config->get('url.jiraKanbanBoard');
$kanbanBoardFilter = $this->config->get('jira.filterFields')->toArray();
$kanbanBoardUri = sprintf(
$kanbanBoardUriParams['baseUrl'],
isset($filterId) ? $filterId : $kanbanBoardUriParams['filterId'],
implode(",", $kanbanBoardUriParams['fields']->toArray())
$kanbanBoardUri,
$team->getFilterId(),
implode(",", $kanbanBoardFilter)
);
$response = $this->httpClient
@ -74,37 +111,196 @@ class JiraCollectorService
->send();
if (!$response->isSuccess()) {
throw new \UnexpectedValueException("Bad JIRA result", $response->getStatusCode());
throw new \UnexpectedValueException(sprintf(
"Bad JIRA result for URL:\n%s",
$kanbanBoardUri
), $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);
$kanbanBoard = $this->hydrateKanbanBoard($team, $parsedJsonData);
$this->cache->setItem(sprintf("%s-%s", self::CACHE_KEY_KANBANBOARD, $teamName), serialize($kanbanBoard));
return $kanbanBoard;
}
return $kanbanBoard;
return unserialize($kanbanBoard);
}
/**
* @param int $teamId
* @param bool $forceReload
* @return array
* @throws \Exception
*/
public function getTeamWatchedIssues(int $teamId, bool $forceReload = false)
{
$team = $this->teamService->getTeam($teamId);
$teamName = $team->getName();
$watchedIssues = $this->cache->getItem(sprintf("%s-%s", self::CACHE_KEY_WATCHED, $teamName));
if ($forceReload || null === $watchedIssues) {
$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());
}
$watchedIssues = $this->hydrateWatchedIssues(Decoder::decode($response->getBody(), Json::TYPE_ARRAY), $members);
$this->cache->setItem(sprintf("%s-%s", self::CACHE_KEY_WATCHED, $teamName), serialize($watchedIssues));
return $watchedIssues;
}
return unserialize($watchedIssues);
}
/**
* @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 null !== $issue->getComment()
? !in_array($issue->getComment()->getSignum(), $members)
: false;
});
}
/**
* @param string $parentKey
* @return null|string
*/
private function getEpicNameFromParent(string $parentKey): ?string
{
if (array_key_exists($parentKey, $this->cachedEpics)) {
return $this->cachedEpics[$parentKey];
}
$user = $this->config->get('jira.user');
$password = $this->config->get('jira.password');
/** @var Config $kanbanBoardUriParams */
$jiraIssueBaseUrl = $this->config->get('url.jiraIssue');
$kanbanBoardFilter = $this->config->get('jira.filterFields')->toArray();
$kanbanBoardFilterString = implode(",", $kanbanBoardFilter);
$jiraIssueUri = sprintf($jiraIssueBaseUrl, $parentKey, $kanbanBoardFilterString);
$response = $this->httpClient
->setUri($jiraIssueUri)
->setAuth($user, $password)
->send();
if (!$response->isSuccess()) {
throw new \UnexpectedValueException("Bad JIRA result: $jiraIssueUri", $response->getStatusCode());
}
$parsedJsonParentData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY);
if ($parsedJsonParentData['fields'][self::EPIC_TICKET_LINK]) {
$jiraIssueUri = sprintf(
$jiraIssueBaseUrl,
$parsedJsonParentData['fields'][self::EPIC_TICKET_LINK],
$kanbanBoardFilterString
);
$response = $this->httpClient
->setUri($jiraIssueUri)
->setAuth($user, $password)
->send();
if (!$response->isSuccess()) {
throw new \UnexpectedValueException("Bad JIRA result", $response->getStatusCode());
}
$parsedJsonEpicData = Decoder::decode($response->getBody(), Json::TYPE_ARRAY);
$this->cachedEpics[$parentKey] = $parsedJsonEpicData['fields'][self::EPIC_NAME_FIELD];
return $this->cachedEpics[$parentKey];
}
$this->cachedEpics[$parentKey] = null;
return null;
}
/**
* @param Team $team
* @param $parsedJsonData
* @return KanbanBoard
* @todo check if avatar has to be locally cached
*/
private function hydrateKanbanBoard($parsedJsonData): KanbanBoard
private function hydrateKanbanBoard(Team $team, $parsedJsonData): KanbanBoard
{
$kanbanBoard = new KanbanBoard();
$teamBacklogColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getBacklogColumn()["jiraStatusName"]);
$teamInprogressColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getInprogressColumn()["jiraStatusName"]);
$teamVerificationColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getVerificationColumn()["jiraStatusName"]);
$teamDoneColumns = explode(self::BACKLOG_FIELD_DELIMITER, $team->getDoneColumn()["jiraStatusName"]);
foreach ($parsedJsonData['issues'] as $jsonIssue) {
set_time_limit(30);
$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'])
@ -143,48 +339,13 @@ class JiraCollectorService
}
}
// 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']);
// epicName: have to fetch 2 extra records
if (isset($jsonIssue['fields'][self::EPIC_TICKET_LINK])) {
$epicName = $this->getEpicNameFromParent($jsonIssue['key']);
$kanbanEntry->setEpicName($epicName);
} elseif (isset($jsonIssue['fields']['parent'])) {
$epicName = $this->getEpicNameFromParent($jsonIssue['fields']['parent']['key']);
$kanbanEntry->setEpicName($epicName);
}
// jira status
@ -222,20 +383,17 @@ class JiraCollectorService
}
$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;
if (in_array($jiraStatus->getName(), $teamBacklogColumns)) {
$kanbanBoard->addInbox($kanbanEntry);
}
elseif (in_array($jiraStatus->getName(), $teamInprogressColumns)) {
$kanbanBoard->addInProgress($kanbanEntry);
}
elseif (in_array($jiraStatus->getName(), $teamVerificationColumns)) {
$kanbanBoard->addVerification($kanbanEntry);
}
elseif (in_array($jiraStatus->getName(), $teamDoneColumns)) {
$kanbanBoard->addDone($kanbanEntry);
}
unset($kanbanEntry);
}

3
src/App/Service/JiraCollectorServiceFactory.php Normal file → Executable file
View File

@ -18,6 +18,7 @@ class JiraCollectorServiceFactory
$httpClient = $container->get(Client::class);
$config = new Config($configArray['app.config']);
$router = $container->get(RouterInterface::class);
return new JiraCollectorService($cache,$httpClient, $config, $router);
$teamService = $container->get(TeamService::class);
return new JiraCollectorService($cache,$httpClient, $config, $router, $teamService);
}
}

0
src/App/Service/SlideManager.php Normal file → Executable file
View File

3
src/App/Service/TeamService.php Normal file → Executable file
View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Service;
use App\Entity\Team;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityNotFoundException;
use Zend\Form\Form;
@ -26,7 +27,7 @@ class TeamService
}
/**
* @return array
* @return Team[]|ArrayCollection
*/
public function listTeams(): array
{

View File

@ -20,6 +20,7 @@
namespace DoctrineExpressiveModule\Hydrator;
use DateTime;
use DateTimeImmutable;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Util\Inflector;
@ -484,9 +485,10 @@ class DoctrineObject extends AbstractHydrator
/**
* Handle various type conversions that should be supported natively by Doctrine (like DateTime)
*
* @param mixed $value
* @param mixed $value
* @param string $typeOfField
* @return DateTime
* @return DateTime|DateTimeImmutable
* @throws \Exception
*/
protected function handleTypeConversions($value, $typeOfField)
{
@ -507,6 +509,23 @@ class DoctrineObject extends AbstractHydrator
$value = new DateTime($value);
}
break;
case 'datetimetz_immutable':
case 'datetime_immutable':
case 'time_immutable':
case 'date_immutable':
if ('' === $value) {
return null;
}
if (is_int($value)) {
$dateTime = new DateTimeImmutable();
$dateTime->setTimestamp($value);
$value = $dateTime;
} elseif (is_string($value)) {
$value = new DateTimeImmutable($value);
}
break;
default:
}