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