From f60319f836037c7775f192a9e19b385fe2e2ec7e Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+github@gmail.com> Date: Fri, 1 Oct 2021 16:18:46 +0200 Subject: [PATCH] fixes #14 --- TracToGitlabPlugin.php | 65 ++++++++++++++++- assets/cardiogram.svg | 1 + composer.json | 3 +- composer.lock | 107 ++++++++++++++++++++++++++- controllers/convert.php | 2 +- controllers/dashboard.php | 149 ++++++++++++++++++++++++++++++++++++++ controllers/issues.php | 11 --- lib/Controller.php | 44 ++--------- lib/GitlabIssue.php | 73 +++++++++++++++++++ plugin.manifest | 2 +- views/dashboard/index.php | 57 +++++++++++++++ 11 files changed, 460 insertions(+), 54 deletions(-) create mode 100644 assets/cardiogram.svg create mode 100644 controllers/dashboard.php create mode 100644 lib/GitlabIssue.php create mode 100644 views/dashboard/index.php diff --git a/TracToGitlabPlugin.php b/TracToGitlabPlugin.php index 2d44cd6..29b9a61 100644 --- a/TracToGitlabPlugin.php +++ b/TracToGitlabPlugin.php @@ -3,6 +3,62 @@ require_once __DIR__ . '/bootstrap.php'; final class TracToGitlabPlugin extends StudIPPlugin implements StandardPlugin, SystemPlugin { + private $container = null; + + public function __construct() + { + parent::__construct(); + + if (!is_object($GLOBALS['user']) || $GLOBALS['user']->id === 'nobody') { + return; + } + + + $this->buildNavigation(); + } + + public function getDIContainer() + { + if ($this->container === null) { + require_once __DIR__ . '/vendor/autoload.php'; + + $this->container = new Pimple\Container(); + + $this->container['trac'] = function () { + return new TracToGitlab\TracLookup(Config::get()->TRAC2GITLAB_TRAC_URL); + }; + + $this->container['gitlab'] = function () { + $builder = new Gitlab\HttpClient\Builder( + new GuzzleHttp\Client(), + new Http\Factory\Guzzle\RequestFactory(), + new Http\Factory\Guzzle\StreamFactory(), + new Http\Factory\Guzzle\UriFactory() + ); + + $client = new Gitlab\Client($builder); + $client->setUrl(Config::get()->TRAC2GITLAB_GITLAB_URL); + $client->authenticate(Config::get()->TRAC2GITLAB_GITLAB_TOKEN, Gitlab\Client::AUTH_HTTP_TOKEN); + return $client; + }; + + $this->container['gitlabProjectId'] = function () { + return Config::get()->TRAC2GITLAB_GITLAB_PROJECT_ID; + }; + + $this->container['gitlabPager'] = function ($c) { + return new Gitlab\ResultPager($c['gitlab']); + }; + + $this->container['parsedown'] = function () { + $parsedown = new Parsedown(); + $parsedown->setSafeMode(true); + return $parsedown; + }; + } + return $this->container; + } + public function getIconNavigation($course_id, $last_visit, $user_id) { return null; @@ -48,8 +104,13 @@ final class TracToGitlabPlugin extends StudIPPlugin implements StandardPlugin, S $this->addStylesheet('assets/style.scss'); $this->addScript('assets/script.js'); - require_once __DIR__ . '/vendor/autoload.php'; - parent::perform($unconsumed); } + + private function buildNavigation() + { + $navigation = new Navigation(_('Dashboard'), PluginEngine::getURL($this, [], 'dashboard')); + $navigation->setImage(Icon::create($this->getPluginURL() . '/assets/cardiogram.svg', Icon::ROLE_NAVIGATION)); + Navigation::addItem('/gitlab-dashboard', $navigation); + } } diff --git a/assets/cardiogram.svg b/assets/cardiogram.svg new file mode 100644 index 0000000..9478909 --- /dev/null +++ b/assets/cardiogram.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489.5 489.5" style="enable-background:new 0 0 489.5 489.5" xml:space="preserve" width="512" height="512"><path d="M0 0v489.5h489.5V0H0zm47.4 47.4h394.7v170.1h-90.5c-3.5 0-7 1.9-8.5 5.1l-11.7 20.2-57.2-170.6c-2.8-8.5-16.1-9.7-19 1.6l-40.4 217.5-40.4-136c-1.9-6.9-12.8-11-18.6-1.2l-28.7 63.3H47.4v-170zm47.7 404.3c-15.5 0-28-12.4-28-28 0-15.5 12.4-28 28-28 15.5 0 28 12.4 28 28s-12.4 28-28 28zm297.6 0c-15.5 0-28-12.4-28-28 0-15.5 12.4-28 28-28 15.5 0 28 12.4 28 28s-12.5 28-28 28zm49.7-89.7h-395V237.3h85.8c3.9 0 7.4-2.3 8.9-5.8l20.6-45.1 44.7 150.3c3 10.1 17.5 9.3 19.4-1L267.6 115l52.1 154.6c3.8 9.7 15.3 7.3 18.3 1.6l19.8-34.2h84.7v125h-.1z" fill="#24437c"/></svg> \ No newline at end of file diff --git a/composer.json b/composer.json index 157075e..7e4a589 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "guzzlehttp/guzzle": "^7.2", "http-interop/http-factory-guzzle": "^1.0", "erusev/parsedown": "^1.7", - "ext-json": "*" + "ext-json": "*", + "pimple/pimple": "^3.4" } } diff --git a/composer.lock b/composer.lock index 9c3ffd5..b4182ad 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "02c21ff582a38f5e0c5b84426a4902f7", + "content-hash": "33d70628ad93e6a887d04087b16605cf", "packages": [ { "name": "clue/stream-filter", @@ -1048,6 +1048,59 @@ }, "time": "2020-07-07T09:29:14+00:00" }, + { + "name": "pimple/pimple", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "86406047271859ffc13424a048541f4531f53601" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/86406047271859ffc13424a048541f4531f53601", + "reference": "86406047271859ffc13424a048541f4531f53601", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.4.0" + }, + "time": "2021-03-06T08:28:00+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -1097,6 +1150,54 @@ }, "time": "2016-08-06T20:24:11+00:00" }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, { "name": "psr/http-client", "version": "1.0.1", @@ -1606,7 +1707,9 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-json": "*" + }, "platform-dev": [], "plugin-api-version": "2.1.0" } diff --git a/controllers/convert.php b/controllers/convert.php index 0073e4e..d6c9278 100644 --- a/controllers/convert.php +++ b/controllers/convert.php @@ -35,7 +35,7 @@ final class ConvertController extends TracToGitlab\Controller $migrated = TracToGitlab\TicketIssueEntry::findOneByTrac_ticket_id($ticket_id); if ($migrated) { $issue = $this->gitlab->issues()->show( - Config::get()->TRAC2GITLAB_GITLAB_PROJECT_ID, + $this->gitlabProjectId, $migrated['gitlab_issue_id'] ); diff --git a/controllers/dashboard.php b/controllers/dashboard.php new file mode 100644 index 0000000..f9344f1 --- /dev/null +++ b/controllers/dashboard.php @@ -0,0 +1,149 @@ +<?php +final class DashboardController extends TracToGitlab\Controller +{ + const QM_LABEL_MAPPING = [ + 'Anwendungs-Doku' => 'userdoc', + 'Code-Review' => 'code', + 'Entwicklungs-Doku' => 'devdoc', + 'Funktionalität' => 'func', + 'GUI-Richtlinien' => 'gui', + 'Schnittstellen' => 'iface', + 'Textstrings' => 'text', + ]; + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + PageLayout::setTitle(_('Übersicht der Issues in gitlab')); + + $this->setupSidebar(); + } + + private function getSelected(string $what) + { + if (!isset($_SESSION['GITLAB_DASHBOARD_SELECTION'])) { + $_SESSION['GITLAB_DASHBOARD_SELECTION'] = [ + 'milestone' => array_reverse(array_keys($this->getMilestonesAsSelection()))[0], + 'types' => 'TIC,StEP', + ]; + } + return $_SESSION['GITLAB_DASHBOARD_SELECTION'][$what] ?? null; + } + + private function setSelected(string $what, string $value) + { + if ($value !== $this->getSelected($what)) { + $_SESSION['GITLAB_DASHBOARD_SELECTION'][$what] = $value; + } + } + + public function index_action() + { + $this->issues = $this->getIssues(); + $this->mapping = $this->getQMLabelMapping(); + } + + public function select_action(string $what, string $value = null) + { + $this->setSelected($what, Request::get($what, $value)); + + $this->redirect($this->indexURL()); + } + + private function setupSidebar() + { + $options = Sidebar::get()->addWidget(new OptionsWidget()); + $options->addSelect( + _('Meilenstein'), + $this->selectURL('milestone'), + 'milestone', + $this->getMilestonesAsSelection(), + $this->getSelected('milestone') + ); + + $options->addSelect( + _('Typ'), + $this->selectURL('types'), + 'types', + [ + 'TIC,StEP' => 'StEPs und TICs', + 'StEP' => 'StEPs', + 'TIC' => 'TICs', + ], + $this->getSelected('types') + ); + } + + private function getIssues(): array + { + $issues = []; + foreach (explode(',', $this->getSelected('types')) as $type) { + $issues = array_merge( + $issues, + $this->gitlabPager->fetchAll( + $this->gitlab->issues(), + 'all', + [ + $this->gitlabProjectId, + [ + 'sort' => 'asc', + 'scope' => 'all', + 'milestone' => $this->getSelected('milestone'), + 'labels' => $type, + ] + ] + ) + ); + } + usort($issues, function ($a, $b) { + return $a['id'] - $b['id']; + }); + return array_map(function ($issue) { + return new TracToGitlab\GitlabIssue($issue); + }, $issues); + } + + private function getMilestones() + { + $milestones = $this->gitlab->milestones()->all( + $this->gitlabProjectId + ); + $milestones = array_filter($milestones, function ($milestone) { + return preg_match('/^Stud\.IP \d+\.\d+$/', $milestone['title']); + }); + return $milestones; + } + + private function getMilestonesAsSelection() + { + $result = []; + foreach ($this->getMilestones() as $milestone) { + $result[$milestone['title']] = $milestone['title']; + } + return $result; + } + + private function getQMLabels() + { + $labels = $this->gitlabPager->fetchAll( + $this->gitlab->projects(), + 'labels', + [$this->gitlabProjectId] + ); + $labels = array_filter($labels, function ($label) { + return strpos($label['name'], 'QM::') === 0; + }); + $labels = array_map(function ($label) { + return substr($label['name'], 4, -3); + }, $labels); + $labels = array_unique($labels); + $labels = array_values($labels); + return $labels; + } + + private function getQMLabelMapping() + { + return self::QM_LABEL_MAPPING; + } +} diff --git a/controllers/issues.php b/controllers/issues.php index 84f44d4..ceab4cc 100644 --- a/controllers/issues.php +++ b/controllers/issues.php @@ -11,17 +11,6 @@ final class IssuesController extends \TracToGitlab\Controller 'StEP' => ['1927f2b86d6b185aa6c6697810ad42f1', '93c84607e687030b249a69cd30ca7bd9', 'StEP%05u: %s'], ]; - public function before_filter(&$action, &$args) - { - parent::before_filter($action, $args); - - $this->registerGenerator('parsedown', function () { - $parsedown = new Parsedown(); - $parsedown->setSafeMode(true); - return $parsedown; - }); - } - public function create_action() { if (!Request::isPost()) { diff --git a/lib/Controller.php b/lib/Controller.php index 7c3ac98..18801cd 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -2,50 +2,22 @@ namespace TracToGitlab; /** - * @property TracToGitlab\TracLookup $trac + * @property \TracToGitlabPlugin $plugin + * @property TracLookup $trac * @property \Gitlab\Client $gitlab + * @property \Gitlab\ResultPager $gitlabPager + * @property int $gitlabProjectId */ abstract class Controller extends \PluginController { - protected $generators = []; - - public function before_filter(&$action, &$args) - { - parent::before_filter($action, $args); - - $this->registerGenerator('trac', function () { - return new TracLookup(\Config::get()->TRAC2GITLAB_TRAC_URL); - }); - $this->registerGenerator('gitlab', function () { - $builder = new \Gitlab\HttpClient\Builder( - new \GuzzleHttp\Client(), - new \Http\Factory\Guzzle\RequestFactory(), - new \Http\Factory\Guzzle\StreamFactory(), - new \Http\Factory\Guzzle\UriFactory() - ); - - $client = new \Gitlab\Client($builder); - $client->setUrl(\Config::get()->TRAC2GITLAB_GITLAB_URL); - $client->authenticate(\Config::get()->TRAC2GITLAB_GITLAB_TOKEN, \Gitlab\Client::AUTH_HTTP_TOKEN); - return $client; - }); - } - public function __get($offset) { - if (!isset($this->generators[$offset])) { - return $this->$offset ?? null; - } + $container = $this->plugin->getDIContainer(); - if (is_callable($this->generators[$offset])) { - $this->generators[$offset] = $this->generators[$offset](); + if (!isset($container[$offset])) { + return $this->$offset ?? null; } - return $this->generators[$offset]; - } - - public function registerGenerator($name, $value) - { - $this->generators[$name] = $value; + return $container[$offset]; } } diff --git a/lib/GitlabIssue.php b/lib/GitlabIssue.php new file mode 100644 index 0000000..0d72c60 --- /dev/null +++ b/lib/GitlabIssue.php @@ -0,0 +1,73 @@ +<?php +namespace TracToGitlab; + +final class GitlabIssue +{ + private $issue; + + public function __construct(array $issue) + { + $this->issue = $issue; + } + + public function __get($offset) + { + if ($offset === 'assignee') { + return implode(', ', array_map(function ($assignee) { + return $assignee['username']; + }, $this->issue['assignees'])); + } + if ($offset === 'type') { + if ($this->isBiest()) { + return 'BIEST'; + } + if ($this->isTic()) { + return 'TIC'; + } + if ($this->isStep()) { + return 'StEP'; + } + return '?'; + } + return $this->issue[$offset] ?? null; + } + + public function isClosed() + { + return $this->issue['state'] !== 'opened'; + } + + public function isBiest() + { + return in_array('BIEST', $this->issue['labels']); + } + + public function isTic() + { + return in_array('TIC', $this->issue['labels']); + } + + public function isStep() + { + return in_array('StEP', $this->issue['labels']); + } + + public function getIconForQMLabel(string $label) + { + foreach ($this->issue['labels'] as $l) { + if (preg_match("/^QM::{$label}::(.)$/", $l, $match)) { + $state = $match[1]; + if ($state === '+') { + return \Icon::create('accept', \Icon::ROLE_STATUS_GREEN); + } + if ($state === '?') { + return \Icon::create('question', \Icon::ROLE_STATUS_YELLOW); + } + if ($state === '-') { + return \Icon::create('decline', \Icon::ROLE_STATUS_RED); + } + } + } + return ''; + } +} diff --git a/plugin.manifest b/plugin.manifest index 9a9c705..bc90b8c 100644 --- a/plugin.manifest +++ b/plugin.manifest @@ -1,5 +1,5 @@ pluginname=Trac to gitlab converter pluginclassname=TracToGitlabPlugin origin=UOL -version=1.0.3 +version=1.1 studipMinVersion=5.0 diff --git a/views/dashboard/index.php b/views/dashboard/index.php new file mode 100644 index 0000000..b4680c2 --- /dev/null +++ b/views/dashboard/index.php @@ -0,0 +1,57 @@ +<table class="default"> + <thead> + <tr> + <th><?= _('Issue') ?></th> + <th><?= _('Typ') ?></th> + <th><?= _('Status') ?></th> + <th><?= _('Titel') ?></th> + <th><?= _('Autor') ?></th> + <th><?= _('Bearbeiter') ?></th> + <? foreach ($mapping as $label => $abbrevation): ?> + <th> + <abbr title="<?= htmlReady($label) ?>"> + <?= htmlReady($abbrevation) ?> + </abbr> + </th> + <? endforeach; ?> + </tr> + </thead> + <tbody> + <? if (!$issues): ?> + <tr> + <td colspan="<?= 6 + count($mapping) ?>" style="text-align: center"> + <?= _('Keine Issues für diesen Meilenstein und Typ') ?> + </td> + </tr> + <? endif; ?> + <? foreach ($issues as $issue): ?> + <tr> + <td> + <a href="<?= htmlReady($issue->web_url) ?>" target="_blank"> + #<?= htmlReady($issue->iid) ?> + </a> + </td> + <td><?= htmlReady($issue->type) ?></td> + <td> + <? if ($issue->isClosed()): ?> + closed + <? else: ?> + open + <? endif; ?> + </td> + <td> + <a href="<?= htmlReady($issue->web_url) ?>" target="_blank"> + <?= htmlReady($issue->title) ?> + </a> + </td> + <td><?= htmlReady($issue->author['username']) ?></td> + <td><?= htmlReady($issue->assignee) ?></td> + <? foreach ($mapping as $label => $abbrevation): ?> + <td> + <?= $issue->getIconForQMLabel($label) ?> + </td> + <? endforeach; ?> + </tr> + <? endforeach; ?> + </tbody> +</table> -- GitLab