Add tests, unify input.

yt-rest-api
Lio Novelli 2023-01-01 23:41:40 +01:00
parent 4dcbcb228a
commit ff2e35bb93
18 changed files with 534 additions and 166 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/scratch /scratch
.phpcs-cache .phpcs-cache
*~undo-tree~ *~undo-tree~
/app/tests/data/output

View File

@ -17,8 +17,7 @@
** Install/Getting started ** Install/Getting started
Get ~.phar~ file from r, run configuration wizzard (planned for version 1.0). Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0).
Before version 1.0 is released you have to navigate into ~/app~ directory Before version 1.0 is released you have to navigate into ~/app~ directory
and call command through ~.rprt.php~ file. and call command through ~.rprt.php~ file.
@ -46,7 +45,6 @@
- hourly wage - hourly wage
- folder for reports - folder for reports
** Components ** Components
- report service - provides a csv of a report - report service - provides a csv of a report
- local csv file read (provided via argument) - local csv file read (provided via argument)
@ -77,18 +75,31 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
5 days of development. 5 days of development.
- remove errors from reports - remove errors from reports
* Development
** Testing
Main source at symfony [[https://symfony.com/doc/current/console.html#testing-commands][console command page]]. If you read thouroughly:
When using the Console component in a standalone project, use Application and
extend the normal \PHPUnit\Framework\TestCase.
** Plan ** Plan
*** most current *** most current
1. For version 0.6.7 1. For version 0.6.7
- [X] data value objects - [X] data value objects
- phar file - [X] phar file
- [X] additional expenses - [X] additional expenses
2. For version 1.0 2. For version 1.0
- plugin system - plugin system
- for time tracking services - for time tracking services
- for reports - for reports
- symfony/config & symfony/di components - symfony/config & symfony/di components
3. Time tracking service
- youtrack-api
- jira-api
- kimai (https://www.kimai.org/documentation/timesheet.html)
*** current *** current
@ -126,7 +137,6 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
2. configuration wizard 2. configuration wizard
** Learning ** Learning
- https://www.youtube.com/watch?v=aCqM9YnjTe0 - https://www.youtube.com/watch?v=aCqM9YnjTe0
- Choices (~new ChoiceQuestion~) - Choices (~new ChoiceQuestion~)
@ -134,8 +144,8 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
** API calls ** API calls
*** Get csv file *** Get csv file
~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~ ~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~

View File

@ -24,14 +24,15 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"RprtCli\\": "src" "RprtCli\\": ["src", "tests"]
} }
}, },
"require-dev": { "require-dev": {
"squizlabs/php_codesniffer": "^3.7", "squizlabs/php_codesniffer": "^3.7",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"opsway/psr12-strict-coding-standard": "^0.5.0", "opsway/psr12-strict-coding-standard": "^0.5.0",
"phpcompatibility/php-compatibility": "^9.3" "phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.9"
}, },
"scripts": { "scripts": {
"cs": "phpcs --colors --standard=PSR12", "cs": "phpcs --colors --standard=PSR12",
@ -41,5 +42,6 @@
"allow-plugins": { "allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true "dealerdirect/phpcodesniffer-composer-installer": true
} }
} },
"minimum-stability": "alpha"
} }

63
app/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "7b0be09c8f282dfcceb075d1455caa28", "content-hash": "eb0c874610e45fa74d8ccc691f5bd9f8",
"packages": [ "packages": [
{ {
"name": "doctrine/lexer", "name": "doctrine/lexer",
@ -3287,6 +3287,65 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types", "description": "PHPDoc parser with support for nullable, intersection and generic types",
"time": "2020-08-03T20:32:43+00:00" "time": "2020-08-03T20:32:43+00:00"
}, },
{
"name": "phpstan/phpstan",
"version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.9.4"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2022-12-17T13:33:52+00:00"
},
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "9.2.15", "version": "9.2.15",
@ -4930,7 +4989,7 @@
} }
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "alpha",
"stability-flags": [], "stability-flags": [],
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,

View File

@ -38,11 +38,13 @@ use function var_export;
/** /**
* Main file - invoice command. * Main file - invoice command.
*/ */
class InvoiceCommand extends Command class InvoiceCommand extends Command {
{
use SelectReportTrait;
protected $csv; protected $csv;
protected $configuration; protected $config;
protected $youtrack; protected $youtrack;
@ -54,14 +56,14 @@ class InvoiceCommand extends Command
public function __construct( public function __construct(
ReportCsvInterface $csv, ReportCsvInterface $csv,
ConfigurationInterface $configuration, ConfigurationInterface $configuration,
YoutrackInterface $youtrack, YoutrackInterface $trackingService,
PdfExportInterface $pdf_export, PdfExportInterface $pdf_export,
MailerInterface $mailer, MailerInterface $mailer,
?string $name = null ?string $name = null
) { ) {
$this->csv = $csv; $this->csv = $csv;
$this->configuration = $configuration; $this->config = $configuration;
$this->youtrack = $youtrack; $this->trackingService = $trackingService;
$this->pdfExport = $pdf_export; $this->pdfExport = $pdf_export;
$this->mailer = $mailer; $this->mailer = $mailer;
parent::__construct($name); parent::__construct($name);
@ -82,12 +84,12 @@ class InvoiceCommand extends Command
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.' 'Specify the input csv file to generate report from.'
); );
$this->addOption( // $this->addOption(
'youtrack', // 'youtrack',
'y', // 'y',
InputOption::VALUE_NONE, // InputOption::VALUE_NONE,
'Use youtrack api to get a report. If this option is used --file does not have any effect..' // 'Use youtrack api to get a report. If this option is used --file does not have any effect..'
); // );
$this->addOption( $this->addOption(
'pdf', 'pdf',
'p', 'p',
@ -150,59 +152,33 @@ class InvoiceCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) : int protected function execute(InputInterface $input, OutputInterface $output) : int
{ {
if ($input->getOption('test')) { if ($input->getOption('test')) {
$test = $this->youtrack->testYoutrackapi(); $test = $this->trackingService->testYoutrackapi();
$output->writeln($test); $output->writeln($test);
} }
if ($input->getOption('list-reports')) { if ($input->getOption('list-reports')) {
$list = $this->youtrack->listReports(); $list = $this->trackingService->listReports();
$output->writeln(var_export($list, true)); $output->writeln(var_export($list, true));
return Command::SUCCESS; return Command::SUCCESS;
} }
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) { // Gets report parameter.
if ($report = $input->getOption('report')) { $this->getReportParameter($input, $output, 'tracking_service.youtrack.invoice.report');
$this->youtrack->setReportId($report); $report_id = $this->trackingService->getReportId();
} else { $file = $this->getReportCsvFilePath($input, $output, $report_id);
$reports = $this->youtrack->listReports(); $report_name = $this->trackingService->getReportName();
$count = 1;
foreach ($reports as $id => $name) {
$output->writeln("[{$count}] {$name} ({$id})");
$count++;
}
$report = readline('Select id of the report: ');
// Asume people are literate.
$this->youtrack->setReportId($report);
}
if ($output->isVerbose()) {
$output->writeln("Setting report: <info>{$report}</info>.");
}
}
if ($youtrack = $input->getOption('youtrack')) {
$report_id = $this->youtrack->getReportId();
$cache_clear_status = $this->youtrack->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
}
$file = $this->youtrack->downloadReport($report_id);
}
if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) { if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) {
$expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE); $expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE);
} }
if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) { if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) {
// @TODO Add option for custom time tracking data.
$custom = $this->getCustomWorkOrExpenses($input->getOption('custom'), self::TYPE_WORK); $custom = $this->getCustomWorkOrExpenses($input->getOption('custom'), self::TYPE_WORK);
} }
if ($youtrack || $file = $input->getOption('file')) { $output->writeln("report: <info>{$report_name}</info>");
// Youtrack can also provide a file name.
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
$data = $this->csv->getInvoiceData($file); $data = $this->csv->getInvoiceData($file);
if (! empty($expenses)) { if (! empty($expenses)) {
$data = array_merge($data, $expenses); $data = array_merge($data, $expenses);
} }
// $table = $this->generateTable($output, $data);
$table = $this->getTable($output, $data); $table = $this->getTable($output, $data);
$table->render(); $table->render();
if ($input->getOption('pdf')) { if ($input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data); $nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
// @TODO method gatherTokens(); // @TODO method gatherTokens();
@ -213,9 +189,6 @@ class InvoiceCommand extends Command
// Notify the user where the file was generated to. // Notify the user where the file was generated to.
$output->writeln("The file was generated at <info>${output_path}</info>."); $output->writeln("The file was generated at <info>${output_path}</info>.");
} }
// return Command::SUCCESS;
}
if ($input->getOption('send') && $output_path) { if ($input->getOption('send') && $output_path) {
// @TODO If no output path print an error. // @TODO If no output path print an error.
// Send email to configured address. // Send email to configured address.
@ -267,7 +240,7 @@ class InvoiceCommand extends Command
'Price', 'Price',
]); ]);
[$rows, $totalHours, $totalPrice] = [[], 0, 0]; [$rows, $totalHours, $totalPrice] = [[], 0, 0];
$projectsConfig = $this->configuration->get('projects'); $projectsConfig = $this->config->get('projects');
foreach ($projectsConfig as $name => $config) { foreach ($projectsConfig as $name => $config) {
if (! isset($data[$name])) { if (! isset($data[$name])) {
// @TODO Proper error handling. // @TODO Proper error handling.
@ -353,8 +326,7 @@ class InvoiceCommand extends Command
return $output; return $output;
} }
protected function getCustomWorkOrExpenses($custom, $type) protected function getCustomWorkOrExpenses($custom, $type) {
{
$output = []; $output = [];
if (is_string($custom)) { if (is_string($custom)) {
foreach (explode(';', $custom) as $item) { foreach (explode(';', $custom) as $item) {

View File

@ -14,14 +14,14 @@ use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use function array_flip;
use function is_array; use function is_array;
use function is_null; use function is_null;
class ReportCommand extends Command class ReportCommand extends Command {
{
use SelectReportTrait;
protected $trackingService; protected $trackingService;
protected $config; protected $config;
@ -37,22 +37,30 @@ class ReportCommand extends Command
parent::__construct($name); parent::__construct($name);
} }
protected function configure() : void protected function configure() : void {
{
$this->setName('report'); $this->setName('report');
$this->setDescription('Get a time-tracking report into command line.'); $this->setDescription('Get a time-tracking report into command line.');
$this->addOption( $this->addOption(
'report', 'report',
'r', 'r',
InputOption::VALUE_OPTIONAL, InputOption::VALUE_OPTIONAL,
'Select a report from list of your reports' 'Select a report from list ofo your reports'
); );
// Not supported by by ReportCsv service!
// @TODO Build factory for time tracking service (youtrack-csv-export,
// youtrack-api, jira-api, kimai-api (biro.radiostudent.si)).
$this->addOption( $this->addOption(
'time-range', 'time-range',
't', 't',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'Calculates report from tracking service work items directly for time range' 'Calculates report from tracking service work items directly for time range'
); );
$this->addOption(
'file',
'f',
InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.'
);
} }
protected function execute(InputInterface $input, OutputInterface $output) : int protected function execute(InputInterface $input, OutputInterface $output) : int
@ -61,50 +69,19 @@ class ReportCommand extends Command
// @TODO: Implement time range option: // @TODO: Implement time range option:
// - Request workTime items from tracking service // - Request workTime items from tracking service
// - Filter them, join by issue, project ... // - Filter them, join by issue, project ...
// This will came in other report service that will gather data
// directly from cache.
if ($output->isVerbose()) { if ($output->isVerbose()) {
$output->writeln("Time range: {$timeRange}"); $output->writeln("Time range: {$timeRange}");
} }
$output->writeln('<error>This option is not supported yet.</error>'); $output->writeln('<error>This option is not supported yet.</error>');
return Command::FAILURE; return Command::FAILURE;
} }
$reports = $this->trackingService->listReports(); $this->getReportParameter($input, $output);
// Could just parse a csv file or actually get workItems from youtrack ...
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if (! $report = $input->getOption('report')) {
// @TODO Make selection nicer.
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
$helper = $this->getHelper('question');
$question = new ChoiceQuestion('Select report:', $reports);
$question
->setErrorMessage('Report %s does not exist!')
->setAutocompleterValues($reports);
$report = $helper->ask($input, $output, $question);
$output->writeln('Report ' . $report . ' selected.');
}
// If parameter option is not recognised check if report name was given.
if (! isset($reports[$report])) {
if (! isset(array_flip($reports)[$report])) {
$output->writeln('Non-existing report ' . $report . '. Exiting.');
return Command::SUCCESS;
}
$report = array_flip($reports)[$report];
}
$this->trackingService->setReportId($report);
} elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) {
$this->trackingService->setReportId($report);
}
// Currently we only support csv download. // Currently we only support csv download.
$report_id = $this->trackingService->getReportId(); $report_id = $this->trackingService->getReportId();
$report_name = $reports[$report_id]; $file = $this->getReportCsvFilePath($input, $output, $report_id);
// Code duplication. $report_name = $this->trackingService->getReportName();
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report cache cleared, status: {$cache_clear_status}");
}
$file = $this->trackingService->downloadReport($report_id);
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
$output->writeln("report: <info>{$report_name}</info>"); $output->writeln("report: <info>{$report_name}</info>");
$data = $this->csv->generateReportTable($file); $data = $this->csv->generateReportTable($file);
$table = $this->buildTable($output, $data); $table = $this->buildTable($output, $data);

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace RprtCli\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* Trait to select report.
*
* To be used on commands with trackingService property and config property.
* Command must have report input option. It is not the most elegant solution
* but it helps avoiding code duplication.
*/
trait SelectReportTrait {
protected function checkContext() {
if (!isset($this->trackingService)) {
return FALSE;
}
return $this instanceof Command;
}
protected function getReportParameter(InputInterface $input, OutputInterface $output, $default = 'tracking_service.youtrack.report.report') {
if (!$this->checkContext()) {
return Command::FAILURE;
}
$reports = $this->trackingService->listReports();
// Could just parse a csv file or actually get workItems from youtrack ...
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if (! $report_id = $input->getOption('report')) {
// @TODO Make selection nicer.
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
$helper = $this->getHelper('question');
$question = new ChoiceQuestion('Select report:', $reports);
$question
->setErrorMessage('Report %s does not exist!')
->setAutocompleterValues($reports);
$report_id = $helper->ask($input, $output, $question);
$output->writeln('Report ' . $report_id . ' selected.');
}
// If parameter option is not recognised check if report name was given.
if (! isset($reports[$report_id])) {
if (! isset(array_flip($reports)[$report_id])) {
$output->writeln('Non-existing report ' . $report_id . '. Exiting.');
return Command::FAILURE;
}
$report_id = array_flip($reports)[$report_id];
}
$this->trackingService->setReportId($report_id);
} elseif ($report_id = $this->config->get($default)) {
// If report is not set, try getting default report from configuration.
// This is dependant on the config.
$this->trackingService->setReportId($report_id);
}
$this->trackingService->setReportName();
}
protected function getReportCsvFilePath(InputInterface $input, OutputInterface $output, ?string $report_id) : ?string {
if (!$file = $input->getOption('file')) {
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
}
$file = $this->trackingService->downloadReport($report_id);
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
} else {
$this->trackingService->setReportName($file);
}
return $file;
}
}

View File

@ -188,10 +188,11 @@ class ReportCsv implements ReportCsvInterface
if (empty($data)) { if (empty($data)) {
return []; return [];
} }
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []]; $explodeMinus = fn($ticket) => explode('-', $ticket)[0] ?? 'UNKNOWN';
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], [$explodeMinus($data[0]['id'])]];
foreach ($data as $line) { foreach ($data as $line) {
$project = explode('-', $line['id'])[0]; $project = $explodeMinus($line['id']);
$previous_project = explode('-', $previous)[0]; $previous_project = $explodeMinus($previous);
if ($project !== $previous_project) { if ($project !== $previous_project) {
// When project changes, add a sum of time for that project. // When project changes, add a sum of time for that project.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM; $table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
@ -242,32 +243,4 @@ class ReportCsv implements ReportCsvInterface
return $output; return $output;
} }
/**
* Should be moved into test class.
*/
protected function dummyConfig() : array
{
return [
'projects' => [
'LDP' => [
'name' => 'lupus.digital',
'pattern' => 'LDP-[0-9]+',
'price' => 25,
// optional specify columns
],
'WV' => [
'name' => 'Wirtschaftsverlag',
'pattern' => 'WV-[0-9]+',
'price' => 25,
// optional specify columns
],
'Other' => [
'name' => 'Other projects',
'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
'price' => 25,
// optional specify columns
],
],
];
}
} }

View File

@ -34,4 +34,8 @@ interface YoutrackInterface
* Array of reports with ids as keys and names as values. * Array of reports with ids as keys and names as values.
*/ */
public function listReports() : array; public function listReports() : array;
public function setReportId(string $report_id) :void;
public function setReportName(?string $report_name = NULL) :void;
public function getReportName() : ?string;
} }

View File

@ -28,14 +28,15 @@ use function var_dump;
class YoutrackService implements YoutrackInterface class YoutrackService implements YoutrackInterface
{ {
protected $ytToken; protected string $ytToken;
protected $ytBaseUrl; protected string $ytBaseUrl;
protected $config; protected ConfigurationInterface $config;
protected $httpClient; protected ClientInterface $httpClient;
protected $report_id; protected string $report_id;
protected ?string $reportName = NULL;
public function __construct(ConfigurationInterface $config, ClientInterface $http_client) public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
{ {
@ -106,6 +107,23 @@ class YoutrackService implements YoutrackInterface
$this->report_id = $report_id; $this->report_id = $report_id;
} }
public function setReportName(?string $report_name = NULL) : void {
if (!$report_name) {
$reports = $this->listReports();
if ($report_id = $this->getReportId()) {
$report_name = $reports[$report_id] ?? NULL;
}
else {
$report_name = NULL;
}
}
$this->reportName = $report_name;
}
public function getReportName() : ?string {
return $this->reportName;
}
public function downloadReport(string $report_id) : ?string public function downloadReport(string $report_id) : ?string
{ {
$path = "youtrack/api/reports/$report_id/export/csv"; $path = "youtrack/api/reports/$report_id/export/csv";

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace RprtCli\Tests\Kernel;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use DI\ContainerBuilder;
use RprtCli\Commands\InvoiceCommand;
use RprtCli\Commands\ReportCommand;
class ReportCommandTest extends TestCase {
public function testExecute() {
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../test-dependencies.php');
$container = $builder->build();
$application = new Application('Command Line Tool to process Youtrack Reports', '0.1.0');
$invoiceCommand = $container->get(InvoiceCommand::class);
$application->add($invoiceCommand);
$reportCommand = $container->get(ReportCommand::class);
$application->add($reportCommand);
$command = $application->find('report');
$commandTester = new CommandTester($command);
$commandTester->execute([
// pass arguments to the helper
'--file' => __DIR__ . '/../data/21-03.csv',
// prefix the key with two dashes when passing options,
// e.g: '--some-option' => 'option_value',
// use brackets for testing array value,
// e.g: '--some-option' => ['option_value'],
]);
$commandTester->assertCommandIsSuccessful();
// the output of the command in the console
$output = $commandTester->getDisplay();
var_dump($output);
$this->assertStringContainsString('21-03.csv', $output);
// ...
}
}

View File

@ -0,0 +1,53 @@
Group name,Item,Item Summary,Estimation time,Spent time
-,DEV-46,"Wochenplanung, Weekly",0,315
-,DEV-56,Developer training,0,60
-,DEV-122,Daily standup,0,255
-,INF-6,"Infrastructure misc, diverse",0,75
-,KUR-181,Sprint meetings (PM),240,390
-,KUR-507,(TKT-307) Change webhook integration for Slack,0,330
-,KUR-552,(TKT-740) Allow kicking users out of collections,780,45
-,KUR-554,(TKT-1448) Adaptions in Backend,0,30
-,KUR-556,[story] (TKT-1393) Make Paragraph embedding more user friendly,0,75
-,KUR-558,(TKT-1467) Full copy of article doesn't update the timestamp,0,15
-,KUR-562,(TKT-1441) Implement SEO title in BE,0,45
-,LDP-668,Add json-ld schema.org metadata to articles,0,300
-,LDP-685,"LDP-Contentpool sitemaps. Extend ""simple_sitemap_extended"" setup to support per front-end site sitemaps.",0,15
-,LDP-692,Output json-ld script,0,30
-,LDP-693,Add WebPage schema.org support,120,180
-,LDP-700,LDP onboarding Somebody,0,15
-,LDP-706,Backend FAQ Marketing Block,0,165
-,LDP-707,"Backend ""Hero"" Marketing Block",0,1035
-,LDP-709,"Backend ""Newsletter Signup"" Marketing Block",0,195
-,LDP-711,"Backend ""Call to action"" Marketing Block",0,165
-,LDP-712,"Backend ""Feature section"" Marketing Block",0,300
-,LDP-713,Provide a field type and pre-configuration for choosing icons,300,45
-,LDP-716,Entity browser UX is not as good as expected,0,15
-,LDP-720,Improve display of nodes and add support for article heros,0,45
-,LDP-728,ldp-marketing blocks break the page when they are added to the layout builder,0,195
-,LDP-729,ldp-cp portal entity,0,480
-,LDP-740,CP: Add portal path prefixes,480,30
-,LDP-741,Defects marketing Blocks,0,450
-,LDP-742,[Red Alert] Develop branch fails to build,0,45
-,VAL-8,Plan hosting setup on custom hosting provider,0,45
-,WV-31,"Internal sprint meetings (PM, fixed-price).",0,150
-,WV-4304,"(ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages.",600,750
-,WV-4305,(ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles),0,60
-,WV-4326,(ODT-1174) Make image optional in the Call to Action block,135,60
-,WV-4355,(ODT-1270) Add article title to breadcrumbs,0,270
-,WV-4359,"(ODT-1254) Display the text paragraph ""zwischentitel"" in H2",0,90
-,WV-4372,(ODT-1297) Branch analytics for articles associated with specific branch (GA),135,15
-,WV-4384,(ODT-1306) Layout-builder rendering in Chrome browser,120,120
-,WV-4393,WVsat docker build error,0,90
-,WV-4410,(ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results.,375,30
-,WV-4411,Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions,600,2070
-,WV-4412,Implement custom dynamic variants for sitemaps to group by month,0,105
-,WV-4414,Invalid JSON in the cache_tools composer.json file,0,60
-,WV-4421,WV Satellites Hand bau image file entities are not replicated,0,15
-,WV-4422,"[Red Alert] Behat test fail. ""Demo content loaded..Error pages work.""",0,75
-,WV-4423,(ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished,0,15
-,WV-4425,Unpublished Advertorials are returned,0,30
-,WV-4427,"(ODT-1332) ""Og: image"" is not specified explicitly",120,15
-,WV-4437,"(ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system.",0,30
-,WV-4442,(ODT-1341) Custom HTML block is not rendered,0,120
-,WV-4478,(ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure.,0,90
-,WV-4480,(ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems,0,90
1 Group name Item Item Summary Estimation time Spent time
2 - DEV-46 Wochenplanung, Weekly 0 315
3 - DEV-56 Developer training 0 60
4 - DEV-122 Daily standup 0 255
5 - INF-6 Infrastructure misc, diverse 0 75
6 - KUR-181 Sprint meetings (PM) 240 390
7 - KUR-507 (TKT-307) Change webhook integration for Slack 0 330
8 - KUR-552 (TKT-740) Allow kicking users out of collections 780 45
9 - KUR-554 (TKT-1448) Adaptions in Backend 0 30
10 - KUR-556 [story] (TKT-1393) Make Paragraph embedding more user friendly 0 75
11 - KUR-558 (TKT-1467) Full copy of article doesn't update the timestamp 0 15
12 - KUR-562 (TKT-1441) Implement SEO title in BE 0 45
13 - LDP-668 Add json-ld schema.org metadata to articles 0 300
14 - LDP-685 LDP-Contentpool sitemaps. Extend "simple_sitemap_extended" setup to support per front-end site sitemaps. 0 15
15 - LDP-692 Output json-ld script 0 30
16 - LDP-693 Add WebPage schema.org support 120 180
17 - LDP-700 LDP onboarding Somebody 0 15
18 - LDP-706 Backend FAQ Marketing Block 0 165
19 - LDP-707 Backend "Hero" Marketing Block 0 1035
20 - LDP-709 Backend "Newsletter Signup" Marketing Block 0 195
21 - LDP-711 Backend "Call to action" Marketing Block 0 165
22 - LDP-712 Backend "Feature section" Marketing Block 0 300
23 - LDP-713 Provide a field type and pre-configuration for choosing icons 300 45
24 - LDP-716 Entity browser UX is not as good as expected 0 15
25 - LDP-720 Improve display of nodes and add support for article heros 0 45
26 - LDP-728 ldp-marketing blocks break the page when they are added to the layout builder 0 195
27 - LDP-729 ldp-cp portal entity 0 480
28 - LDP-740 CP: Add portal path prefixes 480 30
29 - LDP-741 Defects marketing Blocks 0 450
30 - LDP-742 [Red Alert] Develop branch fails to build 0 45
31 - VAL-8 Plan hosting setup on custom hosting provider 0 45
32 - WV-31 Internal sprint meetings (PM, fixed-price). 0 150
33 - WV-4304 (ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages. 600 750
34 - WV-4305 (ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles) 0 60
35 - WV-4326 (ODT-1174) Make image optional in the Call to Action block 135 60
36 - WV-4355 (ODT-1270) Add article title to breadcrumbs 0 270
37 - WV-4359 (ODT-1254) Display the text paragraph "zwischentitel" in H2 0 90
38 - WV-4372 (ODT-1297) Branch analytics for articles associated with specific branch (GA) 135 15
39 - WV-4384 (ODT-1306) Layout-builder rendering in Chrome browser 120 120
40 - WV-4393 WVsat docker build error 0 90
41 - WV-4410 (ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results. 375 30
42 - WV-4411 Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions 600 2070
43 - WV-4412 Implement custom dynamic variants for sitemaps to group by month 0 105
44 - WV-4414 Invalid JSON in the cache_tools composer.json file 0 60
45 - WV-4421 WV Satellites Hand bau image file entities are not replicated 0 15
46 - WV-4422 [Red Alert] Behat test fail. "Demo content loaded..Error pages work." 0 75
47 - WV-4423 (ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished 0 15
48 - WV-4425 Unpublished Advertorials are returned 0 30
49 - WV-4427 (ODT-1332) "Og: image" is not specified explicitly 120 15
50 - WV-4437 (ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system. 0 30
51 - WV-4442 (ODT-1341) Custom HTML block is not rendered 0 120
52 - WV-4478 (ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure. 0 90
53 - WV-4480 (ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems 0 90

View File

@ -0,0 +1,10 @@
Hello,
I'm sending you the invoice for [[month]] [[year]].
This email and invoice were generated and sent by (rprt-cli)[https://git.kompot.si/lio/RprtCli] (WIP).
Kind regards
Liopold Novelli

View File

@ -0,0 +1,91 @@
<html>
<head>
<style>
body {
font-family: sans-serif;
font-size: 12pt;
}
.table {
width: 100%;
display: flex;
justify-content: space-between;
}
.table table {
border: 1px solid;
border-radius: 5px;
width: 100%;
margin: 0px auto;
float: none;
}
p { margin: 0pt; }
td { vertical-align: top; }
table td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
table tbody td {
background-color: #FFF;
border: 0.1mm solid #000000;
padding-right: 2mm;
padding-left: 2mm;
}
tbody td.td-3,
tbody td.td-1,
.right {
text-align: right;
}
tbody td.td-2,
.center {
text-align: center;
}
table td.colspan.td-2 {
text-align: left;
}
table thead th,
table tr.bold td,
table tr.bold td.td-3 {
background-color: #EEEEEE;
border: 0.1mm solid #000000;
font-weight: bold;
}
table tr.center td,
table tr.center td.td-3,
table tbody tr.center td.td-2.colspan {
text-align: center!important;
}
/*
tbody tr.tr-2 td,
tbody tr.tr-3 td {
background-color: #EEE;
border-top: 0.4mm solid #000;
}
tbody tr.tr-3 td {
font-weight: bold;
}
*/
.left {
text-align: left;
}
</style>
</head>
<body>
<p class="center">Spletni razvoj, Liopold Doron Novelli s.p.,<br>ADDRESS<br>NUMBER, liopold@drunomics.com<br>NUMBER</p>
<p><br><br></p>
<p>Company name<br>Company Name<br>Wien<br>Austria</p>
<p><br></p>
<p><br></p>
<p><br></p>
<p class="right">Laibach, am [[today]]</p>
<p><strong>Honorarnote<br>Nummer [[number]]</strong></p>
<p><br></p>
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:<br></p>
<p><br></p>
<div class="table">[[table]]</div>
<p><br>Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer ACCOUNT NUMBER mit der Bankleitzahl BANK zu überweisen.<br></p>
<p><br>Vielen Dank für den Auftrag,<br>
mit besten Grüßen,</p>
<p><br><br></p>
<p><br><br></p>
<p>Liopold Doron Novelli</p>
</body>
</html>

View File

@ -0,0 +1,47 @@
tracking_service:
youtrack:
auth_token: 'perm:<secret>'
base_url: 'https://<organization>.myjetbrains.com'
report_id: '83-554'
# report command
report:
report: '83-541'
# invoice command
invoice:
# default value for report parameter
report: '83-554'
export:
template_path: 'tests/data/invoice-template.html'
output: 'tests/data/output/Invoice-Novelli-[[month]]-[[year]].pdf'
labels:
- 'Tätigkeit'
- 'Stunden'
- 'Stundensatz'
- 'EUR'
email:
template_path: 'tests/data/rprt-cli/email-template.txt'
username: '<username>'
tokens:
me: 'Liopold Novelli'
to:
- 'wolfgang.ziegler@drunomics.com'
- 'accounting@drunomics.com'
from: 'liopolddoron@gmail.com'
projects:
LDP:
name: 'lupus.digital publishing'
pattern: 'LDP'
price: 25.6
currency: 'EUR'
project_column: 1
time_column: 4
# time format m - minutes, h - hours
time_format: 'm'
Other:
name: 'Projektbezogene Entwicklung'
pattern: '(?!.\bLDP\b)'
price: 29.1
currency: 'EUR'
project_column: 1
time_column: 4
time_format: 'm'

View File

@ -0,0 +1,7 @@
<?php
$dependencies = require_once(__DIR__ . '/../dependencies.php');
$dependencies['config.path'] = __DIR__ . '/data';
return $dependencies;

View File

@ -18,6 +18,20 @@ TODO:
- phing for build automation - phing for build automation
- add ddev for dockerization - add ddev for dockerization
TODO (end of 2022):
- DONE fix project list output (first is missing)
- DONE report selection trait
- automated tests
- phpcs
- phpstan
OPTIONAL:
- phing build system to run quality assurance
- DONE fix -y parameter in invoice
- time track command
- report from api service and factory:
- youtrack
- jira
- kimai