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:
@@ -1,5 +1,5 @@
|
||||
unreleased=true
|
||||
future-release=1.13
|
||||
future-release=1.14
|
||||
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
|
||||
issues-wo-labels=false
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13)
|
||||
|
||||
|
||||
55
Dockerfile
55
Dockerfile
@@ -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
|
||||
21
README.md
21
README.md
@@ -1,5 +1,3 @@
|
||||
<h1 align="center">Kimai 2 - online time-tracker</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kimai/images/master/repository-header.png" alt="Kimai logo">
|
||||
</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>
|
||||
</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.
|
||||
|
||||
## 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
|
||||
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use
|
||||
- [Translations](https://www.kimai.org/documentation/translations.html) - Kimai in your language
|
||||
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use Kimai
|
||||
- [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
|
||||
|
||||
### 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
|
||||
- Webserver (nginx, Apache)
|
||||
- A modern browser
|
||||
- Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/)
|
||||
|
||||
### About
|
||||
|
||||
This is the new version of the open source timetracker Kimai. It is stable and production ready, ships
|
||||
with most advanced features from Kimai 1 and many new ones, including but not limited to:
|
||||
This is the new version of the open source time tracker Kimai. It is stable and production ready, ships
|
||||
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,
|
||||
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
|
||||
|
||||
- [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
|
||||
|
||||
### Plugins
|
||||
|
||||
@@ -7,8 +7,8 @@ As announced in the [README](README.md) I only support the latest available rele
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| master | :white_check_mark: |
|
||||
| 1.13 | :white_check_mark: |
|
||||
| < 1.13 | :x: |
|
||||
| 1.14 | :white_check_mark: |
|
||||
| < 1.14 | :x: |
|
||||
|
||||
## 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.
|
||||
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.
|
||||
I am grateful for any (discrete) disclosure of vulnerabilities!
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kevinpapst/kimai2",
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"description": "Kimai Time-Tracking 2",
|
||||
"description": "Kimai - Time Tracking",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2",
|
||||
"php": "^7.2.9",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
|
||||
1224
composer.lock
generated
1224
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ doctrine:
|
||||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
override_url: true
|
||||
# existing migrations will fail if the schema filter is activated
|
||||
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
|
||||
url: '%env(DATABASE_URL)%'
|
||||
|
||||
@@ -26,7 +26,6 @@ fos_rest:
|
||||
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
|
||||
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 404
|
||||
'App\API\NotFoundException': 404
|
||||
'Pagerfanta\Exception\OutOfRangeCurrentPageException': 404
|
||||
body_listener:
|
||||
enabled: true
|
||||
decoders:
|
||||
@@ -45,7 +44,7 @@ handcraftedinthealps_rest_routing:
|
||||
routing_loader:
|
||||
default_format: 'json'
|
||||
prefix_methods: true
|
||||
include_format: true
|
||||
include_format: false
|
||||
formats:
|
||||
json: true
|
||||
xml: false
|
||||
|
||||
@@ -42,9 +42,9 @@ nelmio_api_doc:
|
||||
- ^/api(?!/doc)
|
||||
documentation:
|
||||
info:
|
||||
title: Kimai 2 - API Docs
|
||||
title: Kimai - API Docs
|
||||
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'
|
||||
securityDefinitions:
|
||||
apiUser:
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\HttpKernel\Exception\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);
|
||||
}
|
||||
|
||||
45
src/EventSubscriber/PagerfantaExceptionSubscriber.php
Normal file
45
src/EventSubscriber/PagerfantaExceptionSubscriber.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,27 +28,25 @@ use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Defines the form used to manipulate Timesheet entries.
|
||||
* @internal
|
||||
* Helper functions to manage dependent customer-project-activity fields.
|
||||
*/
|
||||
trait FormTrait
|
||||
{
|
||||
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null)
|
||||
{
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'mapped' => false,
|
||||
'project_enabled' => true,
|
||||
]);
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'mapped' => false,
|
||||
'project_enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null)
|
||||
@@ -102,17 +100,15 @@ trait FormTrait
|
||||
|
||||
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null)
|
||||
{
|
||||
$builder
|
||||
->add('activity', ActivityType::class, [
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) {
|
||||
$query = new ActivityFormTypeQuery($activity, $project);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
$builder->add('activity', ActivityType::class, [
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) {
|
||||
$query = new ActivityFormTypeQuery($activity, $project);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
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
|
||||
$builder->addEventListener(
|
||||
@@ -143,20 +139,23 @@ trait FormTrait
|
||||
{
|
||||
@trigger_error('FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead', E_USER_DEPRECATED);
|
||||
|
||||
$builder
|
||||
->add('description', DescriptionType::class, [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
]
|
||||
]);
|
||||
$builder->add('description', DescriptionType::class, [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.14
|
||||
*/
|
||||
protected function addTags(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder
|
||||
->add('tags', TagsType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
@trigger_error('FormTrait::addTags() is deprecated and will be removed with 2.0, use TagsType instead', E_USER_DEPRECATED);
|
||||
|
||||
$builder->add('tags', TagsType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\TagsType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\CustomerRepository;
|
||||
@@ -121,8 +122,7 @@ class TimesheetEditForm extends AbstractType
|
||||
$descriptionOptions['attr'] = ['autofocus' => 'autofocus'];
|
||||
}
|
||||
$builder->add('description', DescriptionType::class, $descriptionOptions);
|
||||
|
||||
$this->addTags($builder);
|
||||
$builder->add('tags', TagsType::class, ['required' => false]);
|
||||
$this->addRates($builder, $currency, $options);
|
||||
$this->addUser($builder, $options);
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
@@ -69,6 +69,36 @@ final class MarkdownExtension implements RuntimeExtensionInterface
|
||||
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 .= ' …';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the timesheet description content into HTML.
|
||||
*
|
||||
|
||||
@@ -44,6 +44,7 @@ class RuntimeExtensions extends AbstractExtension
|
||||
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('comment2html', [MarkdownExtension::class, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('comment1line', [MarkdownExtension::class, 'commentOneLiner'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
"doctrine/dbal": {
|
||||
"version": "v2.6.3"
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "v0.5.3"
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "1.6",
|
||||
"recipe": {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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 %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{% for project in projects %}
|
||||
<tr class="{{ widgets.class_project_row(project, now) }}">
|
||||
<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">{{ widgets.label_visible(project.visible) }}</td>
|
||||
<td class="w-min text-center">{{ widgets.badge_team_access(project.teams) }}</td>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
{% endif %}
|
||||
<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, '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, 'company') }}">{{ entry.company }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{% for activity in activities %}
|
||||
<tr class="{{ widgets.class_activity_row(activity, now) }}">
|
||||
<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.badge_team_access(activity.teams) }}</td>
|
||||
<td class="actions">{{ actions.activity(activity, 'custom') }}</td>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<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, '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, '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>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'exported': {'class': 'text-center hidden w-min', 'orderBy': false},
|
||||
'actions': {'class': 'actions alwaysVisible'},
|
||||
}) %}
|
||||
|
||||
@@ -162,6 +163,9 @@
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}">
|
||||
{{ widgets.label_boolean(entry.exported) }}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{{ block('row_action') }}
|
||||
</td>
|
||||
|
||||
@@ -60,21 +60,8 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param bool $json
|
||||
* @return string
|
||||
*/
|
||||
protected function createUrl($url, $json = true)
|
||||
protected function createUrl(string $url): string
|
||||
{
|
||||
if ($json) {
|
||||
if (stripos($url, '?') !== false) {
|
||||
$url = str_replace('?', '.json?', $url);
|
||||
} else {
|
||||
$url .= '.json';
|
||||
}
|
||||
}
|
||||
|
||||
return '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class ApiDocControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$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');
|
||||
$swaggerJson = json_decode($result->text(), true);
|
||||
$tags = [];
|
||||
@@ -51,17 +51,13 @@ class ApiDocControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$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);
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function createUrl($url)
|
||||
protected function createUrl(string $url): string
|
||||
{
|
||||
return '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
26
tests/API/NotFoundExceptionTest.php
Normal file
26
tests/API/NotFoundExceptionTest.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -79,11 +79,7 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function createUrl($url)
|
||||
protected function createUrl(string $url): string
|
||||
{
|
||||
return '/' . self::DEFAULT_LANGUAGE . '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
62
tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php
Normal file
62
tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,42 @@ class MarkdownExtensionTest extends TestCase
|
||||
$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 …',
|
||||
$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. …',
|
||||
$sut->commentOneLiner(implode(PHP_EOL, [$loremIpsum, $loremIpsum, $loremIpsum]), true)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'Lorem ipsum dolor sit amet, consetetur sadipscing …',
|
||||
$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 …',
|
||||
$sut->commentOneLiner(implode(PHP_EOL, ['Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt', 'ssdf']), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class RuntimeExtensionsTest extends TestCase
|
||||
{
|
||||
public function testGetFilters()
|
||||
{
|
||||
$expected = ['md2html', 'desc2html', 'comment2html'];
|
||||
$expected = ['md2html', 'desc2html', 'comment2html', 'comment1line'];
|
||||
$i = 0;
|
||||
|
||||
$sut = new RuntimeExtensions();
|
||||
@@ -67,6 +67,7 @@ class RuntimeExtensionsTest extends TestCase
|
||||
$found_md2html = false;
|
||||
$found_desc2html = false;
|
||||
$found_comment2html = false;
|
||||
$found_comment1line = false;
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
switch ($filter->getName()) {
|
||||
@@ -85,11 +86,17 @@ class RuntimeExtensionsTest extends TestCase
|
||||
self::assertEquals(['html'], $filters[2]->getSafe(new Node()));
|
||||
$found_comment2html = true;
|
||||
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_desc2html, 'Missing filter: desc2html');
|
||||
self::assertTrue($found_comment2html, 'Missing filter: comment2html');
|
||||
self::assertTrue($found_comment1line, 'Missing filter: comment1line');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user