release 1.17.2 (#3135)
* allow to input password interactively in console * added block to simplify overwriting export template parts * prevent installation in PHP 8.1 * composer update * phpunit version to 9 * support negative amounts in excel export * added system configuration actions for calendar, weekly timesheet, users * added method to render text with full markdown support
This commit is contained in:
2
.github/workflows/coverage.yaml
vendored
2
.github/workflows/coverage.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.4']
|
||||
php: ['8.0']
|
||||
|
||||
name: Coverage (${{ matrix.php }})
|
||||
steps:
|
||||
|
||||
2
.github/workflows/testing.yaml
vendored
2
.github/workflows/testing.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
php: ['7.4', '8.0', '8.1']
|
||||
|
||||
name: Integration (${{ matrix.php }})
|
||||
steps:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.3",
|
||||
"php": "7.3.*||7.4.*||8.0.*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
@@ -78,12 +78,12 @@
|
||||
"dama/doctrine-test-bundle": "^6.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"fakerphp/faker": "^1.15",
|
||||
"friendsofphp/php-cs-fixer": "3.2.*",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-symfony": "^1.0",
|
||||
"phpunit/phpunit": "^8.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"symfony/browser-kit": "^4.4",
|
||||
"symfony/css-selector": "^4.4",
|
||||
"symfony/debug-bundle": "^4.4",
|
||||
|
||||
1765
composer.lock
generated
1765
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php">
|
||||
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<ini name="max_execution_time" value="-1"/>
|
||||
@@ -23,7 +20,6 @@
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
|
||||
<!--
|
||||
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
||||
|
||||
@@ -41,10 +37,11 @@
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">templates/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">src/Migrations/</directory>
|
||||
<directory suffix=".php">assets/</directory>
|
||||
@@ -57,8 +54,7 @@
|
||||
<directory suffix=".php">var/</directory>
|
||||
<directory suffix=".php">vendor/</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</coverage>
|
||||
|
||||
<!--
|
||||
wrap tests inside database transactions, so tests can safely manipulate contents without
|
||||
|
||||
40
src/Command/AbstractUserCommand.php
Normal file
40
src/Command/AbstractUserCommand.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
abstract class AbstractUserCommand extends Command
|
||||
{
|
||||
protected function askForPassword(InputInterface $input, OutputInterface $output): string
|
||||
{
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$passwordQuestion = new Question('Please enter the password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
$passwordQuestion->setValidator(function (?string $value) {
|
||||
$password = trim($value);
|
||||
if (empty($password)) {
|
||||
throw new \Exception('The password may not be empty');
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$passwordQuestion->setMaxAttempts(3);
|
||||
|
||||
return $helper->ask($input, $output, $passwordQuestion);
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,11 @@ namespace App\Command;
|
||||
use App\User\UserService;
|
||||
use App\Utils\CommandStyle;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ChangePasswordCommand extends Command
|
||||
final class ChangePasswordCommand extends AbstractUserCommand
|
||||
{
|
||||
private $userService;
|
||||
|
||||
@@ -35,7 +34,7 @@ class ChangePasswordCommand extends Command
|
||||
->setDescription('Change the password of a user.')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
new InputArgument('password', InputArgument::REQUIRED, 'The password'),
|
||||
new InputArgument('password', InputArgument::OPTIONAL, 'The password'),
|
||||
])
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
@@ -59,7 +58,12 @@ EOT
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$username = $input->getArgument('username');
|
||||
|
||||
if (null !== $input->getArgument('password')) {
|
||||
$password = $input->getArgument('password');
|
||||
} else {
|
||||
$password = $this->askForPassword($input, $output);
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByUsernameOrThrowException($username);
|
||||
|
||||
|
||||
@@ -13,14 +13,11 @@ use App\Entity\User;
|
||||
use App\User\UserService;
|
||||
use App\Utils\CommandStyle;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
final class CreateUserCommand extends Command
|
||||
final class CreateUserCommand extends AbstractUserCommand
|
||||
{
|
||||
private $userService;
|
||||
|
||||
@@ -91,31 +88,4 @@ final class CreateUserCommand extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function askForPassword(InputInterface $input, OutputInterface $output): string
|
||||
{
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$passwordQuestion = new Question('Please enter the password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
$passwordQuestion->setValidator(function (?string $value) {
|
||||
$password = trim($value);
|
||||
if (empty($password)) {
|
||||
throw new \Exception('The password may not be empty');
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$passwordQuestion->setMaxAttempts(3);
|
||||
|
||||
return $helper->ask($input, $output, $passwordQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ final class ThemeConfiguration implements \ArrayAccess
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return $this->systemConfiguration->has('theme.' . $offset);
|
||||
}
|
||||
@@ -42,7 +39,7 @@ final class ThemeConfiguration implements \ArrayAccess
|
||||
* @param mixed $value
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
throw new \BadMethodCallException('ThemeConfiguration does not support offsetSet()');
|
||||
}
|
||||
@@ -51,7 +48,7 @@ final class ThemeConfiguration implements \ArrayAccess
|
||||
* @param mixed $offset
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
throw new \BadMethodCallException('ThemeConfiguration does not support offsetUnset()');
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ class CalendarSubscriber extends AbstractActionsSubscriber
|
||||
$event->addCreate($this->path('timesheet_create'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('system_configuration')) {
|
||||
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'calendar']), 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
|
||||
$event->addHelp($this->documentationLink('calendar.html'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ class QuickEntrySubscriber extends AbstractActionsSubscriber
|
||||
|
||||
public function onActions(PageActionsEvent $event): void
|
||||
{
|
||||
if ($this->isGranted('system_configuration')) {
|
||||
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'quick_entry']), 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
|
||||
$event->addHelp($this->documentationLink('weekly-times.html'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ class UsersSubscriber extends AbstractActionsSubscriber
|
||||
$event->addCreate($this->path('admin_user_create'), false);
|
||||
}
|
||||
|
||||
if ($this->isGranted('system_configuration')) {
|
||||
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'user']), 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
|
||||
$event->addHelp($this->documentationLink('users.html'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ abstract class AbstractSpreadsheetRenderer
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
public const DURATION_DECIMAL = '#0.00';
|
||||
// https://support.microsoft.com/de-de/office/zahlenformatcodes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
|
||||
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
|
||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
|
||||
|
||||
protected $durationFormat = self::DURATION_FORMAT;
|
||||
|
||||
@@ -49,11 +49,11 @@ class PDFRenderer
|
||||
*/
|
||||
private $pdfOptions = [];
|
||||
|
||||
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectRepository)
|
||||
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectStatisticService)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
$this->converter = $converter;
|
||||
$this->projectStatisticService = $projectRepository;
|
||||
$this->projectStatisticService = $projectStatisticService;
|
||||
}
|
||||
|
||||
protected function getTemplate(): string
|
||||
|
||||
@@ -53,7 +53,6 @@ class TimesheetEditForm extends AbstractType
|
||||
$project = null;
|
||||
$customer = null;
|
||||
$currency = false;
|
||||
$begin = null;
|
||||
$customerCount = $this->customers->countCustomer(true);
|
||||
$timezone = $options['timezone'];
|
||||
$isNew = true;
|
||||
|
||||
@@ -126,6 +126,6 @@ final class MarkdownExtension implements RuntimeExtensionInterface
|
||||
*/
|
||||
public function markdownToHtml(string $content): string
|
||||
{
|
||||
return $this->markdown->toHtml($content);
|
||||
return $this->markdown->withFullMarkdownSupport($content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ class Duration
|
||||
* @param string $format
|
||||
* @return string|null
|
||||
*/
|
||||
public function format($seconds, $format = self::FORMAT_NO_SECONDS)
|
||||
public function format(?int $seconds, string $format = self::FORMAT_NO_SECONDS)
|
||||
{
|
||||
if (null === $seconds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hour = floor($seconds / 3600);
|
||||
$minute = floor(($seconds / 60) % 60);
|
||||
$hour = (int) floor($seconds / 3600);
|
||||
$minute = (int) floor((int) ($seconds / 60) % 60);
|
||||
|
||||
$hour = $hour > 9 ? $hour : '0' . $hour;
|
||||
$minute = $minute > 9 ? $minute : '0' . $minute;
|
||||
@@ -60,7 +60,7 @@ class Duration
|
||||
* @param string $duration
|
||||
* @return int
|
||||
*/
|
||||
public function parseDurationString($duration): int
|
||||
public function parseDurationString(string $duration): int
|
||||
{
|
||||
if (false !== stripos($duration, ':')) {
|
||||
return $this->parseDuration($duration, self::FORMAT_COLON);
|
||||
|
||||
@@ -83,7 +83,6 @@ class MPdfConverter implements HtmlToPdfConverter
|
||||
for ($i = 0; $i < \count($parts); $i++) {
|
||||
if (stripos($parts[$i], '<!-- CONTENT_PART -->') !== false) {
|
||||
$subParts = explode('<!-- CONTENT_PART -->', $parts[$i]);
|
||||
$run = 0;
|
||||
foreach ($subParts as $subPart) {
|
||||
$mpdf->WriteHTML($subPart);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Utils;
|
||||
|
||||
/**
|
||||
* A simple class to parse markdown syntax and return HTML.
|
||||
* Parse markdown syntax and return HTML.
|
||||
*/
|
||||
final class Markdown
|
||||
{
|
||||
@@ -19,20 +19,14 @@ final class Markdown
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
public function __construct()
|
||||
public function toHtml(string $text, bool $safe = true): string
|
||||
{
|
||||
if ($this->parser === null) {
|
||||
$this->parser = new ParsedownExtension();
|
||||
$this->parser->setUrlsLinked(true);
|
||||
$this->parser->setBreaksEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param bool $safe
|
||||
* @return string
|
||||
*/
|
||||
public function toHtml(string $text, bool $safe = true): string
|
||||
{
|
||||
if ($safe !== true) {
|
||||
@trigger_error('Only safe mode is supported in Markdown since 1.16.3 to prevent XSS attacks. Parameter $safe will be removed with 2.0', E_USER_DEPRECATED);
|
||||
}
|
||||
@@ -42,4 +36,15 @@ final class Markdown
|
||||
|
||||
return $this->parser->text($text);
|
||||
}
|
||||
|
||||
public function withFullMarkdownSupport(string $text): string
|
||||
{
|
||||
$parser = new \Parsedown();
|
||||
$parser->setUrlsLinked(true);
|
||||
$parser->setBreaksEnabled(true);
|
||||
$parser->setSafeMode(true);
|
||||
$parser->setMarkupEscaped(true);
|
||||
|
||||
return $parser->text($text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ParsedownExtension extends \Parsedown
|
||||
protected $BlockTypes = [
|
||||
'*' => ['Rule', 'List'],
|
||||
'+' => ['List'],
|
||||
'-' => ['SetextHeader', 'Table', 'Rule', 'List'],
|
||||
'-' => ['Table', 'Rule', 'List'],
|
||||
'0' => ['List'],
|
||||
'1' => ['List'],
|
||||
'2' => ['List'],
|
||||
@@ -37,7 +37,6 @@ class ParsedownExtension extends \Parsedown
|
||||
'9' => ['List'],
|
||||
':' => ['Table'],
|
||||
'<' => ['Comment', 'Markup'],
|
||||
'=' => ['SetextHeader'],
|
||||
'>' => ['Quote'],
|
||||
'[' => ['Reference'],
|
||||
'_' => ['Rule'],
|
||||
@@ -124,7 +123,7 @@ class ParsedownExtension extends \Parsedown
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function getIDfromText($text)
|
||||
private function getIDfromText($text): string
|
||||
{
|
||||
$text = strtolower($text);
|
||||
|
||||
|
||||
18
symfony.lock
18
symfony.lock
@@ -291,15 +291,15 @@
|
||||
"phpunit/php-file-iterator": {
|
||||
"version": "1.4.5"
|
||||
},
|
||||
"phpunit/php-invoker": {
|
||||
"version": "3.1.1"
|
||||
},
|
||||
"phpunit/php-text-template": {
|
||||
"version": "1.2.1"
|
||||
},
|
||||
"phpunit/php-timer": {
|
||||
"version": "1.0.9"
|
||||
},
|
||||
"phpunit/php-token-stream": {
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "4.7",
|
||||
"recipe": {
|
||||
@@ -333,12 +333,21 @@
|
||||
"robrichards/xmlseclibs": {
|
||||
"version": "3.0.4"
|
||||
},
|
||||
"sebastian/cli-parser": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"sebastian/code-unit": {
|
||||
"version": "1.0.8"
|
||||
},
|
||||
"sebastian/code-unit-reverse-lookup": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"sebastian/comparator": {
|
||||
"version": "2.1.1"
|
||||
},
|
||||
"sebastian/complexity": {
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"sebastian/diff": {
|
||||
"version": "2.0.1"
|
||||
},
|
||||
@@ -351,6 +360,9 @@
|
||||
"sebastian/global-state": {
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"sebastian/lines-of-code": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"sebastian/object-enumerator": {
|
||||
"version": "3.0.3"
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
{{ widgets.user_avatar(comment.createdBy, false, 'direct-chat-img img-sm') }}
|
||||
<div class="direct-chat-text">
|
||||
{{ comment.message|replace(replacer)|md2html }}
|
||||
{{ comment.message|replace(replacer)|comment2html }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -103,7 +103,9 @@
|
||||
<sethtmlpagefooter name="myfooter" value="on" />
|
||||
mpdf-->
|
||||
{% endblock %}
|
||||
{% block body_start %}{% endblock %}
|
||||
{% block summary %}
|
||||
{% block summary_header %}
|
||||
<h2 style="margin-bottom: 4px; padding-bottom: 0">{% block title %}{{ 'export.document_title'|trans }}{% endblock %}</h2>
|
||||
{% if query is defined %}
|
||||
<p>
|
||||
@@ -112,6 +114,7 @@ mpdf-->
|
||||
</p>
|
||||
{% endif %}
|
||||
<h3>{{ 'export.summary'|trans }}</h3>
|
||||
{% endblock %}
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -254,7 +257,9 @@ mpdf-->
|
||||
<pagebreak>
|
||||
{% endblock %}
|
||||
{% block items %}
|
||||
{% block items_header %}
|
||||
<h3>{{ 'export.full_list'|trans }}</h3>
|
||||
{% endblock %}
|
||||
|
||||
{% set duration = 0 %}
|
||||
{% set rate = 0 %}
|
||||
@@ -338,5 +343,6 @@ mpdf-->
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block body_end %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
{% import "invoice/macros.html.twig" as macros %}
|
||||
|
||||
{% set columns = {
|
||||
'date': {'class': 'alwaysVisible'},
|
||||
'date': {'class': 'alwaysVisible text-nowrap'},
|
||||
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
|
||||
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
||||
'customer': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
||||
} %}
|
||||
{% for field in metaColumns %}
|
||||
|
||||
@@ -20,6 +20,7 @@ use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\ChangePasswordCommand
|
||||
* @covers \App\Command\AbstractUserCommand
|
||||
* @group integration
|
||||
*/
|
||||
class ChangePasswordCommandTest extends KernelTestCase
|
||||
@@ -59,6 +60,7 @@ class ChangePasswordCommandTest extends KernelTestCase
|
||||
$input = [
|
||||
'command' => $command->getName(),
|
||||
];
|
||||
$interactive = false;
|
||||
|
||||
if ($username !== null) {
|
||||
$input['username'] = $username;
|
||||
@@ -66,10 +68,19 @@ class ChangePasswordCommandTest extends KernelTestCase
|
||||
|
||||
if ($password !== null) {
|
||||
$input['password'] = $password;
|
||||
} else {
|
||||
$interactive = true;
|
||||
}
|
||||
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute($input);
|
||||
|
||||
$options = [];
|
||||
if ($interactive) {
|
||||
$options = ['interactive' => true];
|
||||
$commandTester->setInputs(['12345678']);
|
||||
}
|
||||
|
||||
$commandTester->execute($input, $options);
|
||||
|
||||
return $commandTester;
|
||||
}
|
||||
@@ -100,11 +111,10 @@ class ChangePasswordCommandTest extends KernelTestCase
|
||||
$this->callCommand(null, '1234567890');
|
||||
}
|
||||
|
||||
public function testWithMissingPassword()
|
||||
public function testWithMissingPasswordAsksForPassword()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Not enough arguments (missing: "password").');
|
||||
|
||||
$this->callCommand('1234567890', null);
|
||||
$commandTester = $this->callCommand('john_user', null);
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] Changed password for user "john_user".', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ use App\Tests\Configuration\TestConfigLoader;
|
||||
use App\Tests\Mocks\Saml\SamlAuthFactoryFactory;
|
||||
use OneLogin\Saml2\Auth;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Util\Xml;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
@@ -83,7 +82,7 @@ class SamlControllerTest extends TestCase
|
||||
|
||||
public function testMetadataAction()
|
||||
{
|
||||
$expected = <<<EOD
|
||||
$expectedXmlString = <<<EOD
|
||||
<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2020-07-23T10:26:50Z" cacheDuration="PT604800S" entityID="https://127.0.0.1:8010/auth/saml/metadata">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
@@ -118,8 +117,13 @@ EOD;
|
||||
self::assertInstanceOf(Response::class, $result);
|
||||
self::assertEquals('xml', $result->headers->get('Content-Type'));
|
||||
|
||||
$expected = Xml::load($expected);
|
||||
$actual = Xml::load($result->getContent());
|
||||
$expected = new \DOMDocument();
|
||||
$tmp = $expected->loadXML($expectedXmlString);
|
||||
self::assertTrue($tmp);
|
||||
|
||||
$actual = new \DOMDocument();
|
||||
$tmp = $actual->loadXML($result->getContent());
|
||||
self::assertTrue($tmp);
|
||||
|
||||
// the "validUntil" attribute in the outer node changes per request
|
||||
self::assertEquals($expected->firstChild->firstChild, $actual->firstChild->firstChild);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Tests\Configuration\TestConfigLoader;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
@@ -33,6 +34,11 @@ class CalendarControllerTest extends ControllerBaseTest
|
||||
$this->request($client, '/calendar/');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertPageActions($client, [
|
||||
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
|
||||
'help' => 'https://www.kimai.org/documentation/calendar.html'
|
||||
]);
|
||||
|
||||
$crawler = $client->getCrawler();
|
||||
$calendar = $crawler->filter('div#timesheet_calendar');
|
||||
$this->assertEquals(1, $calendar->count());
|
||||
@@ -40,6 +46,18 @@ class CalendarControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(1, $dragAndDropBoxes->count());
|
||||
}
|
||||
|
||||
public function testCalendarActionAsSuperAdmin()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/calendar/');
|
||||
|
||||
$this->assertPageActions($client, [
|
||||
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
|
||||
'settings modal-ajax-form' => $this->createUrl('/admin/system-config/edit/calendar'),
|
||||
'help' => 'https://www.kimai.org/documentation/calendar.html'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCalendarActionWithGoogleSource()
|
||||
{
|
||||
$loader = new TestConfigLoader([]);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerMeta;
|
||||
@@ -161,6 +162,13 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('A beautiful and short comment **with some** markdown formatting', $node->html());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
|
||||
$configService->offsetSet('timesheet.markdown_content', true);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('<p>A beautiful and short comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityMeta;
|
||||
use App\Entity\ActivityRate;
|
||||
@@ -260,6 +261,13 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('A beautiful and long comment **with some** markdown formatting', $node->html());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
|
||||
$configService->offsetSet('timesheet.markdown_content', true);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('<p>A beautiful and long comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class UserControllerTest extends ControllerBaseTest
|
||||
'visibility' => '#',
|
||||
'download toolbar-action' => $this->createUrl('/admin/user/export'),
|
||||
'create' => $this->createUrl('/admin/user/create'),
|
||||
'settings modal-ajax-form' => $this->createUrl('/admin/system-config/edit/user'),
|
||||
'help' => 'https://www.kimai.org/documentation/users.html'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Tests\DependencyInjection;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use PHPUnit\Framework\Error\Notice;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
@@ -301,7 +300,7 @@ class AppExtensionTest extends TestCase
|
||||
*/
|
||||
public function testDurationOnlyDeprecationIsTriggered()
|
||||
{
|
||||
$this->expectException(Notice::class);
|
||||
$this->expectNotice();
|
||||
$this->expectExceptionMessage('Found ambiguous configuration: remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
||||
|
||||
$minConfig = $this->getMinConfig();
|
||||
@@ -449,7 +448,7 @@ class AppExtensionTest extends TestCase
|
||||
|
||||
public function testWithBundleConfigurationFailsOnDuplicatedKey()
|
||||
{
|
||||
$this->expectException(Notice::class);
|
||||
$this->expectNotice();
|
||||
$this->expectExceptionMessage('Invalid bundle configuration "timesheet" found, skipping');
|
||||
|
||||
$bundleConfig = [
|
||||
@@ -463,7 +462,7 @@ class AppExtensionTest extends TestCase
|
||||
|
||||
public function testWithBundleConfigurationFailsOnNonArray()
|
||||
{
|
||||
$this->expectException(Notice::class);
|
||||
$this->expectNotice();
|
||||
$this->expectExceptionMessage('Invalid bundle configuration found, skipping all bundle configuration');
|
||||
|
||||
$container = $this->getContainer();
|
||||
|
||||
@@ -163,7 +163,18 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setMetaField((new TimesheetMeta())->setName('foo2')->setValue('meta-bar2')->setIsVisible(true))
|
||||
;
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
|
||||
$timesheet6 = new Timesheet();
|
||||
$timesheet6
|
||||
->setDuration(400)
|
||||
->setFixedRate(-100.92)
|
||||
->setUser((new User())->setUsername('nivek'))
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime('2019-06-16 12:00:00'))
|
||||
->setEnd(new \DateTime('2019-06-16 12:06:40'))
|
||||
;
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6];
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query->setActivities([$activity]);
|
||||
|
||||
@@ -34,7 +34,7 @@ class CsvRendererTest extends AbstractRendererTest
|
||||
public function getTestModel()
|
||||
{
|
||||
return [
|
||||
['400', '2437.12', ' EUR 1,947.99 ', 6, 5, 1, 2, 2]
|
||||
['400', '2437.12', ' EUR 1,947.99 ', 7, 6, 1, 2, 2]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -107,8 +107,40 @@ class CsvRendererTest extends AbstractRendererTest
|
||||
27 => 'ORDER-123',
|
||||
];
|
||||
|
||||
self::assertEquals(6, \count($all));
|
||||
$expected2 = [
|
||||
0 => '2019-06-16',
|
||||
1 => '12:00',
|
||||
2 => '12:06',
|
||||
3 => '400',
|
||||
4 => '0',
|
||||
5 => '',
|
||||
6 => 'nivek',
|
||||
7 => 'nivek',
|
||||
8 => 'Customer Name',
|
||||
9 => 'project name',
|
||||
10 => 'activity description',
|
||||
11 => '',
|
||||
12 => '',
|
||||
13 => '',
|
||||
14 => '',
|
||||
15 => '',
|
||||
16 => ' EUR -100.92',
|
||||
17 => '',
|
||||
18 => '',
|
||||
19 => 'customer-bar',
|
||||
20 => '',
|
||||
21 => 'project-foo2',
|
||||
22 => 'activity-bar',
|
||||
23 => 'timesheet',
|
||||
24 => 'work',
|
||||
25 => 'A-0123456789',
|
||||
26 => 'DE-9876543210',
|
||||
27 => 'ORDER-123',
|
||||
];
|
||||
|
||||
self::assertEquals(7, \count($all));
|
||||
self::assertEquals($expected, $all[5]);
|
||||
self::assertEquals($expected2, $all[6]);
|
||||
self::assertEquals(\count($expected), \count($all[0]));
|
||||
self::assertEquals('foo', $all[4][14]);
|
||||
}
|
||||
|
||||
@@ -82,11 +82,12 @@ class HtmlRendererTest extends AbstractRendererTest
|
||||
|
||||
$this->assertStringContainsString('<td>Customer Name</td>', $content);
|
||||
$this->assertStringContainsString('<td>project name</td>', $content);
|
||||
$this->assertStringContainsString('<span class="duration-format" data-duration="6600">01:50 h</span>', $content);
|
||||
$this->assertStringContainsString('<span class="duration-format" data-duration="7000">01:56 h</span>', $content);
|
||||
$this->assertStringContainsString('<td class="cost summary-rate">€2,437.12</td>', $content);
|
||||
$this->assertStringContainsString('-€100.92', $content);
|
||||
|
||||
// 5 times in the "full list" and once in the "summary with activities"
|
||||
$this->assertEquals(6, substr_count($content, 'activity description'));
|
||||
$this->assertEquals(7, substr_count($content, 'activity description'));
|
||||
$this->assertEquals(1, substr_count($content, '<td>activity description</td>'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class MarkdownExtensionTest extends TestCase
|
||||
$config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
|
||||
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
||||
$this->assertEquals('<p># foobar</p>', $sut->markdownToHtml('# foobar'));
|
||||
$this->assertEquals('<h1>foobar</h1>', $sut->markdownToHtml('# foobar'));
|
||||
$this->assertEquals(
|
||||
'<p><a href="javascript%3Aalert(`XSS`)">XSS</a></p>',
|
||||
$sut->markdownToHtml('[XSS](javascript:alert(`XSS`))')
|
||||
@@ -44,6 +44,9 @@ class MarkdownExtensionTest extends TestCase
|
||||
);
|
||||
$this->assertEquals('', $sut->timesheetContent(null));
|
||||
$this->assertEquals('', $sut->timesheetContent(''));
|
||||
$this->assertEquals('# foobar', $sut->timesheetContent('# foobar'));
|
||||
$this->assertEquals('## foobar', $sut->timesheetContent('## foobar'));
|
||||
$this->assertEquals('### foobar', $sut->timesheetContent('### foobar'));
|
||||
|
||||
$config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
|
||||
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||
@@ -75,6 +78,9 @@ class MarkdownExtensionTest extends TestCase
|
||||
|
||||
$this->assertEquals('', $sut->commentContent(null));
|
||||
$this->assertEquals('', $sut->commentContent(''));
|
||||
$this->assertEquals('# foobar', $sut->commentContent('# foobar'));
|
||||
$this->assertEquals('## foobar', $sut->commentContent('## foobar'));
|
||||
$this->assertEquals('### foobar', $sut->commentContent('### foobar'));
|
||||
$this->assertEquals('<p>' . $loremIpsum . '</p>', $sut->commentContent($loremIpsum, true));
|
||||
$this->assertEquals('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l …', $sut->commentContent($loremIpsum));
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ use Symfony\Component\Dotenv\Dotenv;
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
(new Dotenv(false))->loadEnv(dirname(__DIR__) . '/.env');
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
|
||||
$env = $_SERVER['APP_ENV'] ?? 'prod';
|
||||
$debug = (bool) ($_SERVER['APP_DEBUG'] ?? (in_array($env, ['dev', 'test'])));
|
||||
|
||||
$kernel = new Kernel($env, $debug);
|
||||
$kernel->boot();
|
||||
|
||||
return $kernel->getContainer()->get('doctrine')->getManager();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="he" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<trans-unit id="IbERy.5" resname="default.pdf.twig">
|
||||
<source>Default</source>
|
||||
<target>ברירת מחדל</target>
|
||||
</trans-unit>
|
||||
|
||||
Reference in New Issue
Block a user