prepare release 1.14 (#2495)

* removed un-maintained docker file
* update all packages
* fix deprecations
* only show one line descriptions for customer, projects and activities
* allow to show export column in timesheet listing
This commit is contained in:
Kevin Papst
2021-04-08 18:07:57 +02:00
committed by GitHub
parent 2ce6e815ff
commit af9dea9226
30 changed files with 920 additions and 739 deletions

View File

@@ -1,5 +1,5 @@
unreleased=true unreleased=true
future-release=1.13 future-release=1.14
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
issues-wo-labels=false issues-wo-labels=false

View File

@@ -1,5 +1,8 @@
# Changelog # Changelog
## [1.14](https://github.com/kevinpapst/kimai2/tree/1.14)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.13...1.14)
## [1.13](https://github.com/kevinpapst/kimai2/tree/1.13) ## [1.13](https://github.com/kevinpapst/kimai2/tree/1.13)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13) [Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13)

View File

@@ -1,55 +0,0 @@
# 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.
FROM php:7.2.9-apache-stretch AS tmp_kimai2_base
RUN apt update && \
apt install -y --allow-unauthenticated \
git \
haveged \
libicu-dev \
libjpeg-dev \
libldap2-dev \
libldb-dev \
libpng-dev \
mysql-client \
unzip \
wget \
zip \
&& \
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)" && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" && \
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then >&2 echo 'ERROR: Invalid installer signature'; rm composer-setup.php; exit 1; fi && \
php composer-setup.php --quiet && \
rm composer-setup.php && \
mv /var/www/html/composer.phar /usr/bin/composer && \
docker-php-ext-install \
gd \
intl \
ldap \
pdo_mysql \
zip && \
apt remove -y wget && \
apt -y autoremove && \
apt clean && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
FROM tmp_kimai2_base
RUN git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
sed "s/prod/dev/g" /opt/kimai/.env.dist > /opt/kimai/.env && \
composer install --working-dir=/opt/kimai --dev --optimize-autoloader && \
/opt/kimai/bin/console doctrine:database:create && \
/opt/kimai/bin/console doctrine:schema:create && \
/opt/kimai/bin/console doctrine:migrations:version --add --all && \
/opt/kimai/bin/console cache:warmup && \
chown -R www-data:www-data /opt/kimai/var && \
chown www-data:www-data /opt/kimai/vendor/mpdf/mpdf/tmp
WORKDIR /opt/kimai
EXPOSE 8001
USER www-data
CMD /opt/kimai/bin/console server:run 0.0.0.0:8001

View File

@@ -1,5 +1,3 @@
<h1 align="center">Kimai 2 - online time-tracker</h1>
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/kimai/images/master/repository-header.png" alt="Kimai logo"> <img src="https://raw.githubusercontent.com/kimai/images/master/repository-header.png" alt="Kimai logo">
</p> </p>
@@ -13,29 +11,30 @@
<a href="https://www.bountysource.com/teams/kimai2"><img alt="Bountysource" src="https://img.shields.io/bountysource/team/kimai2/activity"></a> <a href="https://www.bountysource.com/teams/kimai2"><img alt="Bountysource" src="https://img.shields.io/bountysource/team/kimai2/activity"></a>
</p> </p>
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers. <h1 align="center">Kimai 2 - Time Tracker</h1>
Kimai is a free, open source and online time tracking software designed for small businesses and freelancers.
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc. It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
## Introduction ## Introduction
- [Home](https://www.kimai.org) - The house of Kimai - [Home](https://www.kimai.org) - Kimai project homepage
- [Blog](https://www.kimai.org/blog/) - Get the latest news - [Blog](https://www.kimai.org/blog/) - Get the latest news
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use - [Documentation](https://www.kimai.org/documentation/) - Learn how to use Kimai
- [Translations](https://www.kimai.org/documentation/translations.html) - Kimai in your language - [Translations](https://hosted.weblate.org/projects/kimai/#languages) - Kimai in your language
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from v1 - [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from v1
### Requirements ### Requirements
- PHP 7.2.9 or higher - PHP 7.2.9 or higher ([PHP 8 not yet](https://github.com/kevinpapst/kimai2/pull/2158))
- MariaDB or MySQL - MariaDB or MySQL
- Webserver (nginx, Apache) - Webserver (nginx, Apache)
- A modern browser
- Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/) - Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/)
### About ### About
This is the new version of the open source timetracker Kimai. It is stable and production ready, ships This is the new version of the open source time tracker Kimai. It is stable and production ready, ships
with most advanced features from Kimai 1 and many new ones, including but not limited to: with a lot of advanced features, including but not limited to:
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones, JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
authentication via SAML/LDAP/Database, customizable role permissions, responsive and ready for your mobile device, authentication via SAML/LDAP/Database, customizable role permissions, responsive and ready for your mobile device,
@@ -50,7 +49,7 @@ user specific rates, advanced search & filtering, money and time budgets with re
### Updating Kimai ### Updating Kimai
- [Update Kimai](https://www.kimai.org/documentation/updates.html) - the documentation - [Update Kimai](https://www.kimai.org/documentation/updates.html) - get the latest version
- [UPGRADING guide](UPGRADING.md) - version specific steps - [UPGRADING guide](UPGRADING.md) - version specific steps
### Plugins ### Plugins

View File

@@ -7,8 +7,8 @@ As announced in the [README](README.md) I only support the latest available rele
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| master | :white_check_mark: | | master | :white_check_mark: |
| 1.13 | :white_check_mark: | | 1.14 | :white_check_mark: |
| < 1.13 | :x: | | < 1.14 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -17,7 +17,7 @@ Please report any security related vulnerability in the [advisories section at G
I will work as fast as I can to fix the problem and publish a bugfix release / security update. I will work as fast as I can to fix the problem and publish a bugfix release / security update.
Depending on the size of the required fixes, this might take a couple of hours or a couple of days. Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
You can expect that your message will be answered ASAP, but please take into account that I am living in the timezone Europe/Berlin. You can expect that your message will be answered ASAP, but please take into account that I am living in the timezone CET.
If your issue is valid and after I verified and fixed it, you will be mentioned in the release notes. If your issue is valid and after I verified and fixed it, you will be mentioned in the release notes.
I am grateful for any (discrete) disclosure of vulnerabilities! I am grateful for any (discrete) disclosure of vulnerabilities!

View File

@@ -2,7 +2,7 @@
"name": "kevinpapst/kimai2", "name": "kevinpapst/kimai2",
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"description": "Kimai Time-Tracking 2", "description": "Kimai - Time Tracking",
"authors": [ "authors": [
{ {
"name": "Kevin Papst", "name": "Kevin Papst",
@@ -10,7 +10,7 @@
} }
], ],
"require": { "require": {
"php": "^7.2", "php": "^7.2.9",
"ext-gd": "*", "ext-gd": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",

1224
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ doctrine:
default_connection: default default_connection: default
connections: connections:
default: default:
override_url: true
# existing migrations will fail if the schema filter is activated # existing migrations will fail if the schema filter is activated
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~ #schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
url: '%env(DATABASE_URL)%' url: '%env(DATABASE_URL)%'

View File

@@ -26,7 +26,6 @@ fos_rest:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 404 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 404
'App\API\NotFoundException': 404 'App\API\NotFoundException': 404
'Pagerfanta\Exception\OutOfRangeCurrentPageException': 404
body_listener: body_listener:
enabled: true enabled: true
decoders: decoders:
@@ -45,7 +44,7 @@ handcraftedinthealps_rest_routing:
routing_loader: routing_loader:
default_format: 'json' default_format: 'json'
prefix_methods: true prefix_methods: true
include_format: true include_format: false
formats: formats:
json: true json: true
xml: false xml: false

View File

@@ -42,9 +42,9 @@ nelmio_api_doc:
- ^/api(?!/doc) - ^/api(?!/doc)
documentation: documentation:
info: info:
title: Kimai 2 - API Docs title: Kimai - API Docs
description: | description: |
JSON API for the Kimai 2 time-tracking software: [API documentation](https://www.kimai.org/documentation/rest-api.html), [Swagger definition file](doc.json) JSON API for the Kimai time-tracking software: [API documentation](https://www.kimai.org/documentation/rest-api.html), [Swagger definition file](doc.json)
version: '0.6' version: '0.6'
securityDefinitions: securityDefinitions:
apiUser: apiUser:

View File

@@ -15,7 +15,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class NotFoundException extends NotFoundHttpException class NotFoundException extends NotFoundHttpException
{ {
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 0, array $headers = []) public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 404, array $headers = [])
{ {
parent::__construct($message, $previous, $code, $headers); parent::__construct($message, $previous, $code, $headers);
} }

View File

@@ -0,0 +1,45 @@
<?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\EventSubscriber;
use Pagerfanta\Exception\NotValidMaxPerPageException;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Catches Pagerfanta Exceptions and converts them to "normal" Http Exceptions with status code 404.
* This was mainly done to convert the 500 to 404 HTTP response code.
* This prevents also the need to register them in fos_rest.yaml for the API.
*/
final class PagerfantaExceptionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['onCoreException', 1]
];
}
/**
* @param ExceptionEvent $event
*/
public function onCoreException(ExceptionEvent $event)
{
$throwable = $event->getThrowable();
if ($throwable instanceof NotValidMaxPerPageException || $throwable instanceof OutOfRangeCurrentPageException) {
$notFoundHttpException = new NotFoundHttpException($throwable->getMessage(), $throwable, $throwable->getCode());
$event->setThrowable($notFoundHttpException);
}
}
}

View File

@@ -28,15 +28,13 @@ use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
/** /**
* Defines the form used to manipulate Timesheet entries. * Helper functions to manage dependent customer-project-activity fields.
* @internal
*/ */
trait FormTrait trait FormTrait
{ {
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null) protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null)
{ {
$builder $builder->add('customer', CustomerType::class, [
->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) { 'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer); $query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user')); $query->setUser($builder->getOption('user'));
@@ -102,8 +100,7 @@ trait FormTrait
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null) protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null)
{ {
$builder $builder->add('activity', ActivityType::class, [
->add('activity', ActivityType::class, [
'placeholder' => '', 'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) { 'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) {
$query = new ActivityFormTypeQuery($activity, $project); $query = new ActivityFormTypeQuery($activity, $project);
@@ -111,8 +108,7 @@ trait FormTrait
return $repo->getQueryBuilderForFormType($query); return $repo->getQueryBuilderForFormType($query);
}, },
]) ]);
;
// replaces the activity select after submission, to make sure only activities for the selected project are displayed // replaces the activity select after submission, to make sure only activities for the selected project are displayed
$builder->addEventListener( $builder->addEventListener(
@@ -143,8 +139,7 @@ trait FormTrait
{ {
@trigger_error('FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead', E_USER_DEPRECATED); @trigger_error('FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead', E_USER_DEPRECATED);
$builder $builder->add('description', DescriptionType::class, [
->add('description', DescriptionType::class, [
'required' => false, 'required' => false,
'attr' => [ 'attr' => [
'autofocus' => 'autofocus' 'autofocus' => 'autofocus'
@@ -152,10 +147,14 @@ trait FormTrait
]); ]);
} }
/**
* @deprecated since 1.14
*/
protected function addTags(FormBuilderInterface $builder) protected function addTags(FormBuilderInterface $builder)
{ {
$builder @trigger_error('FormTrait::addTags() is deprecated and will be removed with 2.0, use TagsType instead', E_USER_DEPRECATED);
->add('tags', TagsType::class, [
$builder->add('tags', TagsType::class, [
'required' => false, 'required' => false,
]); ]);
} }

View File

@@ -16,6 +16,7 @@ use App\Form\Type\DurationType;
use App\Form\Type\FixedRateType; use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType; use App\Form\Type\HourlyRateType;
use App\Form\Type\MetaFieldsCollectionType; use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\TagsType;
use App\Form\Type\UserType; use App\Form\Type\UserType;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
@@ -121,8 +122,7 @@ class TimesheetEditForm extends AbstractType
$descriptionOptions['attr'] = ['autofocus' => 'autofocus']; $descriptionOptions['attr'] = ['autofocus' => 'autofocus'];
} }
$builder->add('description', DescriptionType::class, $descriptionOptions); $builder->add('description', DescriptionType::class, $descriptionOptions);
$builder->add('tags', TagsType::class, ['required' => false]);
$this->addTags($builder);
$this->addRates($builder, $currency, $options); $this->addRates($builder, $currency, $options);
$this->addUser($builder, $options); $this->addUser($builder, $options);
$builder->add('metaFields', MetaFieldsCollectionType::class); $builder->add('metaFields', MetaFieldsCollectionType::class);

View File

@@ -69,6 +69,36 @@ final class MarkdownExtension implements RuntimeExtensionInterface
return $content; return $content;
} }
/**
* Transforms the entities comment (customer, project, activity ...) into a one-liner.
*
* @param string|null $content
* @param bool $fullLength
* @return string
*/
public function commentOneLiner(?string $content, bool $fullLength = false): string
{
if (empty($content)) {
return '';
}
$addHellip = false;
if (!$fullLength && \strlen($content) > 52) {
$content = trim(substr($content, 0, 50));
$addHellip = true;
}
$content = explode(PHP_EOL, $content);
$result = $content[0];
if (\count($content) > 1 || $addHellip) {
$result .= ' &hellip;';
}
return $result;
}
/** /**
* Transforms the timesheet description content into HTML. * Transforms the timesheet description content into HTML.
* *

View File

@@ -44,6 +44,7 @@ class RuntimeExtensions extends AbstractExtension
new TwigFilter('md2html', [MarkdownExtension::class, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]), new TwigFilter('md2html', [MarkdownExtension::class, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('desc2html', [MarkdownExtension::class, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]), new TwigFilter('desc2html', [MarkdownExtension::class, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('comment2html', [MarkdownExtension::class, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]), new TwigFilter('comment2html', [MarkdownExtension::class, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('comment1line', [MarkdownExtension::class, 'commentOneLiner'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
]; ];
} }
} }

View File

@@ -44,6 +44,9 @@
"doctrine/dbal": { "doctrine/dbal": {
"version": "v2.6.3" "version": "v2.6.3"
}, },
"doctrine/deprecations": {
"version": "v0.5.3"
},
"doctrine/doctrine-bundle": { "doctrine/doctrine-bundle": {
"version": "1.6", "version": "1.6",
"recipe": { "recipe": {

View File

@@ -60,7 +60,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
{% for field in metaColumns %} {% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }} {{ tables.datatable_meta_column(entry, field) }}

View File

@@ -37,7 +37,7 @@
{% for project in projects %} {% for project in projects %}
<tr class="{{ widgets.class_project_row(project, now) }}"> <tr class="{{ widgets.class_project_row(project, now) }}">
<td>{{ widgets.label_project(project) }}</td> <td>{{ widgets.label_project(project) }}</td>
<td class="hidden-xs">{{ project.comment|comment2html }}</td> <td class="hidden-xs">{{ project.comment|comment1line(false) }}</td>
<td class="w-min text-center">{% if project.end is not null %}{{ project.end|date_short }}{% endif %}</td> <td class="w-min text-center">{% if project.end is not null %}{{ project.end|date_short }}{% endif %}</td>
<td class="w-min text-center">{{ widgets.label_visible(project.visible) }}</td> <td class="w-min text-center">{{ widgets.label_visible(project.visible) }}</td>
<td class="w-min text-center">{{ widgets.badge_team_access(project.teams) }}</td> <td class="w-min text-center">{{ widgets.badge_team_access(project.teams) }}</td>

View File

@@ -59,7 +59,7 @@
{% endif %} {% endif %}
<tr class="{{ class }}" data-href="{{ dataHref }}"> <tr class="{{ class }}" data-href="{{ dataHref }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('customer', true, entry.name, null, entry.color) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('customer', true, entry.name, null, entry.color) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>

View File

@@ -33,7 +33,7 @@
{% for activity in activities %} {% for activity in activities %}
<tr class="{{ widgets.class_activity_row(activity, now) }}"> <tr class="{{ widgets.class_activity_row(activity, now) }}">
<td>{{ widgets.label_activity(activity) }}</td> <td>{{ widgets.label_activity(activity) }}</td>
<td class="hidden-xs">{{ activity.comment|comment2html }}</td> <td class="hidden-xs">{{ activity.comment|comment1line(false) }}</td>
<td class="w-min text-center">{{ widgets.label_visible(activity.visible) }}</td> <td class="w-min text-center">{{ widgets.label_visible(activity.visible) }}</td>
<td class="w-min text-center">{{ widgets.badge_team_access(activity.teams) }}</td> <td class="w-min text-center">{{ widgets.badge_team_access(activity.teams) }}</td>
<td class="actions">{{ actions.activity(activity, 'custom') }}</td> <td class="actions">{{ actions.activity(activity, 'custom') }}</td>

View File

@@ -53,7 +53,7 @@
<tr class="{{ class }}" data-href="{{ dataHref }}"> <tr class="{{ class }}" data-href="{{ dataHref }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td>

View File

@@ -45,6 +45,7 @@
}) %} }) %}
{% endfor %} {% endfor %}
{% set columns = columns|merge({ {% set columns = columns|merge({
'exported': {'class': 'text-center hidden w-min', 'orderBy': false},
'actions': {'class': 'actions alwaysVisible'}, 'actions': {'class': 'actions alwaysVisible'},
}) %} }) %}
@@ -162,6 +163,9 @@
{{ tables.datatable_meta_column(entry, field) }} {{ tables.datatable_meta_column(entry, field) }}
</td> </td>
{% endfor %} {% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}">
{{ widgets.label_boolean(entry.exported) }}
</td>
<td class="actions"> <td class="actions">
{{ block('row_action') }} {{ block('row_action') }}
</td> </td>

View File

@@ -60,21 +60,8 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
return $client; return $client;
} }
/** protected function createUrl(string $url): string
* @param string $url
* @param bool $json
* @return string
*/
protected function createUrl($url, $json = true)
{ {
if ($json) {
if (stripos($url, '?') !== false) {
$url = str_replace('?', '.json?', $url);
} else {
$url .= '.json';
}
}
return '/' . ltrim($url, '/'); return '/' . ltrim($url, '/');
} }

View File

@@ -26,7 +26,7 @@ class ApiDocControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/doc'); $this->assertAccessIsGranted($client, '/api/doc');
$this->assertStringContainsString('<title>Kimai 2 - API Docs</title>', $client->getResponse()->getContent()); $this->assertStringContainsString('<title>Kimai - API Docs</title>', $client->getResponse()->getContent());
$result = $client->getCrawler()->filter('script#swagger-data'); $result = $client->getCrawler()->filter('script#swagger-data');
$swaggerJson = json_decode($result->text(), true); $swaggerJson = json_decode($result->text(), true);
$tags = []; $tags = [];
@@ -51,17 +51,13 @@ class ApiDocControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/doc.json'); $this->assertAccessIsGranted($client, '/api/doc.json');
$this->assertStringContainsString('"title":"Kimai 2 - API Docs"', $client->getResponse()->getContent()); $this->assertStringContainsString('"title":"Kimai - API Docs"', $client->getResponse()->getContent());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
} }
/** protected function createUrl(string $url): string
* @param string $url
* @return string
*/
protected function createUrl($url)
{ {
return '/' . ltrim($url, '/'); return '/' . ltrim($url, '/');
} }

View File

@@ -0,0 +1,26 @@
<?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\Tests\API;
use App\API\NotFoundException;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\API\NotFoundException
*/
class NotFoundExceptionTest extends TestCase
{
public function testConstructor()
{
$sut = new NotFoundException();
self::assertEquals('Not found', $sut->getMessage());
self::assertEquals(404, $sut->getCode());
}
}

View File

@@ -79,11 +79,7 @@ abstract class ControllerBaseTest extends WebTestCase
return $client; return $client;
} }
/** protected function createUrl(string $url): string
* @param string $url
* @return string
*/
protected function createUrl($url)
{ {
return '/' . self::DEFAULT_LANGUAGE . '/' . ltrim($url, '/'); return '/' . self::DEFAULT_LANGUAGE . '/' . ltrim($url, '/');
} }

View File

@@ -0,0 +1,62 @@
<?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\Tests\EventSubscriber;
use App\EventSubscriber\PagerfantaExceptionSubscriber;
use Pagerfanta\Exception\NotValidMaxPerPageException;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* @covers \App\EventSubscriber\PagerfantaExceptionSubscriber
*/
class PagerfantaExceptionSubscriberTest extends TestCase
{
public function testGetSubscribedEvents()
{
$events = PagerfantaExceptionSubscriber::getSubscribedEvents();
$this->assertArrayHasKey(KernelEvents::EXCEPTION, $events);
$methodName = $events[KernelEvents::EXCEPTION][0];
$this->assertTrue(method_exists(PagerfantaExceptionSubscriber::class, $methodName));
}
public function testWithExceptions()
{
$sut = new PagerfantaExceptionSubscriber();
$kernel = $this->createMock(HttpKernelInterface::class);
$request = $this->createMock(Request::class);
$exception = new \Exception();
$requestType = HttpKernelInterface::MASTER_REQUEST;
$event = new ExceptionEvent($kernel, $request, $requestType, $exception);
$sut->onCoreException($event);
self::assertSame($exception, $event->getThrowable());
$event = new ExceptionEvent($kernel, $request, $requestType, new NotValidMaxPerPageException('Foo baaaaar!', 999));
$sut->onCoreException($event);
self::assertInstanceOf(NotFoundHttpException::class, $event->getThrowable());
self::assertEquals('Foo baaaaar!', $event->getThrowable()->getMessage());
self::assertEquals(999, $event->getThrowable()->getCode());
self::assertEquals(404, $event->getThrowable()->getStatusCode());
$event = new ExceptionEvent($kernel, $request, $requestType, new OutOfRangeCurrentPageException('Trölölölölölö', 123));
$sut->onCoreException($event);
self::assertInstanceOf(NotFoundHttpException::class, $event->getThrowable());
self::assertEquals('Trölölölölölö', $event->getThrowable()->getMessage());
self::assertEquals(123, $event->getThrowable()->getCode());
self::assertEquals(404, $event->getThrowable()->getStatusCode());
}
}

View File

@@ -77,4 +77,42 @@ class MarkdownExtensionTest extends TestCase
$sut->commentContent("- test\n- foo\n\nfoo __bar__") $sut->commentContent("- test\n- foo\n\nfoo __bar__")
); );
} }
public function testCommentOneLiner()
{
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new SystemConfiguration($loader, []);
$sut = new MarkdownExtension(new Markdown(), $config);
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.';
$this->assertEquals('', $sut->commentOneLiner(null));
$this->assertEquals('', $sut->commentOneLiner(''));
$this->assertEquals('', $sut->commentOneLiner(null, false));
$this->assertEquals('', $sut->commentOneLiner('', true));
$this->assertEquals(
'Lorem ipsum dolor sit amet, consetetur sadipscing &hellip;',
$sut->commentOneLiner($loremIpsum, false)
);
$this->assertEquals(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. &hellip;',
$sut->commentOneLiner(implode(PHP_EOL, [$loremIpsum, $loremIpsum, $loremIpsum]), true)
);
$this->assertEquals(
'Lorem ipsum dolor sit amet, consetetur sadipscing &hellip;',
$sut->commentOneLiner(implode(PHP_EOL, [$loremIpsum, $loremIpsum, $loremIpsum]), false)
);
$this->assertEquals(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt',
$sut->commentOneLiner(implode(PHP_EOL, ['Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt']), true)
);
$this->assertEquals(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt &hellip;',
$sut->commentOneLiner(implode(PHP_EOL, ['Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt', 'ssdf']), true)
);
}
} }

View File

@@ -22,7 +22,7 @@ class RuntimeExtensionsTest extends TestCase
{ {
public function testGetFilters() public function testGetFilters()
{ {
$expected = ['md2html', 'desc2html', 'comment2html']; $expected = ['md2html', 'desc2html', 'comment2html', 'comment1line'];
$i = 0; $i = 0;
$sut = new RuntimeExtensions(); $sut = new RuntimeExtensions();
@@ -67,6 +67,7 @@ class RuntimeExtensionsTest extends TestCase
$found_md2html = false; $found_md2html = false;
$found_desc2html = false; $found_desc2html = false;
$found_comment2html = false; $found_comment2html = false;
$found_comment1line = false;
foreach ($filters as $filter) { foreach ($filters as $filter) {
switch ($filter->getName()) { switch ($filter->getName()) {
@@ -85,11 +86,17 @@ class RuntimeExtensionsTest extends TestCase
self::assertEquals(['html'], $filters[2]->getSafe(new Node())); self::assertEquals(['html'], $filters[2]->getSafe(new Node()));
$found_comment2html = true; $found_comment2html = true;
break; break;
case 'comment1line':
self::assertEquals('html', $filters[3]->getPreEscape());
self::assertEquals(['html'], $filters[3]->getSafe(new Node()));
$found_comment1line = true;
break;
} }
} }
self::assertTrue($found_md2html, 'Missing filter: md2html'); self::assertTrue($found_md2html, 'Missing filter: md2html');
self::assertTrue($found_desc2html, 'Missing filter: desc2html'); self::assertTrue($found_desc2html, 'Missing filter: desc2html');
self::assertTrue($found_comment2html, 'Missing filter: comment2html'); self::assertTrue($found_comment2html, 'Missing filter: comment2html');
self::assertTrue($found_comment1line, 'Missing filter: comment1line');
} }
} }