Release 2.19 (#4922)

This commit is contained in:
Kevin Papst
2024-07-22 17:51:03 +02:00
committed by GitHub
parent ce22520d7f
commit 8788311faf
32 changed files with 585 additions and 372 deletions

View File

@@ -56,7 +56,7 @@ jobs:
run: composer install
- name: Validate Composer
run: composer validate --strict
run: composer validate --strict --no-check-all
- name: Warmup cache
run: APP_ENV=dev bin/console kimai:reload -n

View File

@@ -1,8 +1,26 @@
@include color-mode(dark, true) {
$dark-font-opacity: 0.6;
/*
--tblr-body-bg: #1e222b;
--tblr-bg-surface-tertiary: #1e222b;
--tblr-bg-surface: #1e222b;
--tblr-bg-surface-secondary: #17202b;
//--tblr-muted: #d9dcdf;
*/
.table thead th {
color: inherit;
opacity: $dark-font-opacity;
}
table.dataTable {
th.today {
color: var(--tblr-muted);
}
thead .sorting:after {
opacity: $dark-font-opacity;
}
}
.overlay {
@@ -12,4 +30,16 @@
color: var(--tblr-white);
}
}
/* Fix tabs pills (e.g. user profile) */
.nav {
--tblr-nav-link-color: inherit;
opacity: $dark-font-opacity;
}
.datagrid-title {
color: inherit;
opacity: $dark-font-opacity;
}
.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
color: inherit;
}
}

View File

@@ -66,6 +66,7 @@
"symfony/security-bundle": "^6.0",
"symfony/security-csrf": "^6.0",
"symfony/serializer": "^6.0",
"symfony/string": "v6.4.8",
"symfony/translation": "^6.0",
"symfony/twig-bundle": "^6.0",
"symfony/validator": "^6.0",
@@ -171,7 +172,6 @@
"@tests-integration"
],
"linting": [
"composer validate --strict",
"bin/console lint:container",
"bin/console lint:yaml config --parse-tags",
"bin/console lint:twig templates --show-deprecations",

555
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4008,21 +4008,6 @@ parameters:
count: 2
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$counter of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setCounter\\(\\) expects int, float\\|int given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$counter of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setCounterBillable\\(\\) expects int, float\\|int given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$counter of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setCounterExported\\(\\) expects int, float\\|int given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$datetime of method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createDateTime\\(\\) expects string, mixed given\\.$#"
count: 1

View File

@@ -36,9 +36,14 @@ DirectoryIndex index.php
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
# Sets the HTTP_AUTHORIZATION header removed by Apache
# Sets the HTTP_AUTHORIZATION header removed by Apache, if PHP is installed as CGI
# Required for the API and below are 3 different methods to achieve it
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
# This might be the only required line
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
# Apache 2.4.13+
# CGIPassAuth On
# Redirect to URI without front controller to prevent duplicate content
# (with and without `/index.php`). Only do this redirect on the initial

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"/build/app.41121fe4.js"
],
"css": [
"/build/app.ee4f7e19.css"
"/build/app.a7ba9fd6.css"
]
},
"app-rtl": {
@@ -15,7 +15,7 @@
"/build/app-rtl.97153087.js"
],
"css": [
"/build/app-rtl.db588f2b.css"
"/build/app-rtl.6516df2d.css"
]
},
"export-pdf": {
@@ -73,9 +73,9 @@
"integrity": {
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
"/build/app.41121fe4.js": "sha384-4v3lVToJIcpMJ++4WaIZ5r9rXR5Vb4waSoHcEzx3vOfcLL+1+TN70di7XPzYSW7I",
"/build/app.ee4f7e19.css": "sha384-7E3ujVsOCkGatHu6khaWWqYhPkc85k22zuXZOwOVo1MlWcPSEBhS65Y0qbeWXdAp",
"/build/app.a7ba9fd6.css": "sha384-GfMYf1vYI9zkkywMoxYk6wpC4UyK185zjDBi16qa0237oztrtOuWdBZsKjWOkyeY",
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
"/build/app-rtl.db588f2b.css": "sha384-OwYS82WpkkoVqI38n3gGpvMTiTFT+dUscgVglMw2vcDvIxAe2B3IcXMjHoCGm/gl",
"/build/app-rtl.6516df2d.css": "sha384-5hF5akg98Hnj/bihpbsiTjMyZ3BsAC4+Z1kzqi1O3/U+tgpnQ+JmwsOADDpMld6d",
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",

View File

@@ -1,7 +1,7 @@
{
"build/app.css": "/build/app.ee4f7e19.css",
"build/app.css": "/build/app.a7ba9fd6.css",
"build/app.js": "/build/app.41121fe4.js",
"build/app-rtl.css": "/build/app-rtl.db588f2b.css",
"build/app-rtl.css": "/build/app-rtl.6516df2d.css",
"build/app-rtl.js": "/build/app-rtl.97153087.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.1442bee7.js",

View File

@@ -482,7 +482,7 @@ final class SystemConfiguration
}
/**
* @internal will be made private soon after 2.18.0 - do ot access this method directly, but through getThemeColors()
* @internal will be made private soon after 2.18.0 - do not access this method directly, but through getThemeColors()
*/
public function getThemeColorChoices(): string
{

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.18.0';
public const VERSION = '2.19.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 21800;
public const VERSION_ID = 21900;
/**
* The software name
*/

View File

@@ -90,7 +90,6 @@ final class ContractController extends AbstractController
'boxes' => $controllerEvent->getController(),
'year' => $year,
'user' => $profile,
'form' => $form->createView(),
]);
}
}

View File

@@ -11,7 +11,9 @@ namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Form\QuickEntryForm;
use App\Form\WeekByUserForm;
use App\Model\QuickEntryWeek;
use App\Reporting\WeekByUser\WeekByUser;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository;
use App\Timesheet\FavoriteRecordService;
@@ -25,7 +27,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Controller used to enter times in weekly form.
*/
#[Route(path: '/quick_entry')]
#[IsGranted('quick-entry')]
final class QuickEntryController extends AbstractController
{
@@ -38,18 +39,30 @@ final class QuickEntryController extends AbstractController
{
}
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
public function quickEntry(Request $request, ?string $begin = null): Response
#[Route(path: '/quick_entry/', name: 'quick_entry', methods: ['GET', 'POST'])]
public function quickEntry(Request $request): Response
{
$factory = $this->getDateTimeFactory();
$user = $this->getUser();
$factory = $this->getDateTimeFactory($user);
$defaultDate = $factory->createDateTime();
if ($begin !== null) {
try {
$begin = $factory->createDateTime($begin);
} catch (\Exception $ex) {
$begin = null;
}
}
$values = new WeekByUser();
$values->setUser($user);
$values->setDate($defaultDate);
$weeklyForm = $this->createFormForGetRequest(WeekByUserForm::class, $values, [
'include_user' => $this->isGranted('view_other_timesheet'),
'timezone' => $factory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'attr' => ['name' => 'quick_entry_weekrange_form']
]);
$weeklyForm->submit($request->query->all(), false);
$user = $values->getUser() ?? $user;
$factory = $this->getDateTimeFactory($user);
$begin = $values->getDate();
if ($begin === null) {
$begin = $factory->createDateTime();
@@ -57,7 +70,6 @@ final class QuickEntryController extends AbstractController
$startWeek = $factory->getStartOfWeek($begin);
$endWeek = $factory->getEndOfWeek($begin);
$user = $this->getUser();
$tmpDay = clone $startWeek;
$week = [];
@@ -235,7 +247,7 @@ final class QuickEntryController extends AbstractController
if ($saved) {
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('quick_entry', ['begin' => $begin->format('Y-m-d')]);
return $this->redirectToRoute('quick_entry', ['date' => $begin->format('Y-m-d'), 'user' => $user->getId()]);
}
} catch (\Exception $ex) {
$this->flashUpdateException($ex);
@@ -244,6 +256,8 @@ final class QuickEntryController extends AbstractController
$page = new PageSetup('quick_entry.title');
$page->setHelp('weekly-times.html');
$page->setPaginationForm($weeklyForm);
$page->setActionName('weekly-times');
return $this->render('quick-entry/index.html.twig', [
'page_setup' => $page,

View File

@@ -61,9 +61,11 @@ final class ReportUsersYearController extends AbstractController
$dateTimeFactory = $this->getDateTimeFactory();
$defaultDate = $dateTimeFactory->createStartOfYear();
$isFinancialYear = false;
if (null !== ($financialYear = $systemConfiguration->getFinancialYearStart())) {
$defaultDate = $this->getDateTimeFactory()->createStartOfFinancialYear($financialYear);
$isFinancialYear = true;
}
$values = new YearlyUserList();
@@ -72,6 +74,7 @@ final class ReportUsersYearController extends AbstractController
$form = $this->createFormForGetRequest(YearlyUserListForm::class, $values, [
'timezone' => $dateTimeFactory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'show_range' => $isFinancialYear,
]);
$form->submit($request->query->all(), false);

View File

@@ -11,7 +11,6 @@ namespace App\Form;
use App\Configuration\SystemConfiguration;
use App\Form\Type\QuickEntryWeekType;
use App\Form\Type\WeekPickerType;
use App\Model\QuickEntryWeek;
use App\Validator\Constraints\QuickEntryModel;
use Symfony\Component\Form\AbstractType;
@@ -57,13 +56,6 @@ final class QuickEntryForm extends AbstractType
}
));
$builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'label' => false,
]);
$builder->add('rows', CollectionType::class, [
'label' => false,
'entry_type' => QuickEntryWeekType::class,

View File

@@ -0,0 +1,48 @@
<?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\Form;
use App\Form\Type\UserType;
use App\Form\Type\WeekPickerType;
use App\Reporting\WeekByUser\WeekByUser;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class WeekByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, [
'width' => false,
'include_current_user_if_system_account' => true
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => WeekByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]);
}
}

View File

@@ -25,7 +25,7 @@ class QuickEntryModel
*/
private array $timesheets = [];
public function __construct(private ?User $user = null, private ?Project $project = null, private ?Activity $activity = null)
public function __construct(private User $user, private ?Project $project = null, private ?Activity $activity = null)
{
}
@@ -39,7 +39,7 @@ class QuickEntryModel
return $this->prototype;
}
public function getUser(): ?User
public function getUser(): User
{
return $this->user;
}

View File

@@ -27,7 +27,7 @@ class QuickEntryWeek
{
}
public function addRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
public function addRow(User $user, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
{
$model = $this->createRow($user, $project, $activity);
@@ -36,7 +36,7 @@ class QuickEntryWeek
return $model;
}
public function createRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
public function createRow(User $user, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
{
return new QuickEntryModel($user, $project, $activity);
}

View File

@@ -28,7 +28,7 @@ final class YearlyUserListForm extends AbstractType
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'show_range' => true,
'show_range' => $options['show_range'],
]);
$builder->add('team', TeamType::class, [
'multiple' => false,
@@ -51,6 +51,7 @@ final class YearlyUserListForm extends AbstractType
'start_date' => new \DateTime(),
'csrf_protection' => false,
'method' => 'GET',
'show_range' => false,
]);
}
}

View File

@@ -37,6 +37,7 @@ final class Extensions extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('report_date', [$this, 'buildReportDate']),
new TwigFunction('class_name', [$this, 'getClassName']),
new TwigFunction('iso_day_by_name', [$this, 'getIsoDayByName']),
new TwigFunction('random_color', [$this, 'randomColor']),
@@ -52,6 +53,38 @@ final class Extensions extends AbstractExtension
];
}
public function buildReportDate(string|int $year, string|int $month = 1, string|int $day = 1): \DateTimeImmutable
{
if (\is_string($month)) {
$month = (int) $month;
}
if ($month > 12 || $month < 1) {
throw new \InvalidArgumentException('Unknown month: ' . $month);
}
if ($month < 10) {
$month = '0' . $month;
}
if (\is_string($day)) {
$day = (int) $day;
}
if ($day > 31 || $day < 1) {
throw new \InvalidArgumentException('Unknown day: ' . $day);
}
if ($day < 10) {
$day = '0' . $day;
}
if (\is_string($year)) {
$year = (int) $year;
}
if ($year < 1980 || $year > 2100) {
throw new \InvalidArgumentException('Unknown year: ' . $year);
}
return \DateTimeImmutable::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day); // @phpstan-ignore-line
}
public function formatReportDate(\DateTimeInterface $dateTime): string
{
return $dateTime->format(self::REPORT_DATE);

View File

@@ -189,18 +189,36 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
return $this->getFormatter()->dayName($dateTime, $short);
}
public function getJavascriptConfiguration(User $user): array
public function getJavascriptConfiguration(?User $user = null): array
{
$language = User::DEFAULT_LANGUAGE;
$browserTitle = false;
$id = null;
$name = 'anonymous';
$admin = false;
$superAdmin = false;
$timezone = date_default_timezone_get();
if ($user !== null) {
$browserTitle = (bool) $user->getPreferenceValue('update_browser_title');
$language = $user->getLanguage();
$id = $user->getId();
$name = $user->getDisplayName();
$admin = $user->isAdmin();
$superAdmin = $user->isSuperAdmin();
$timezone = $user->getTimezone();
}
return [
'locale' => $this->locale,
'language' => $user->getLanguage(),
'language' => $language,
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
'formatDate' => $this->localeService->getDateFormat($this->locale),
'defaultColor' => Constants::DEFAULT_COLOR,
'twentyFourHours' => $this->localeService->is24Hour($this->locale),
'updateBrowserTitle' => (bool) $user->getPreferenceValue('update_browser_title'),
'timezone' => $user->getTimezone(),
'user' => ['id' => $user->getId(), 'name' => $user->getDisplayName(), 'admin' => $user->isAdmin(), 'superAdmin' => $user->isSuperAdmin()],
'updateBrowserTitle' => $browserTitle,
'timezone' => $timezone,
'user' => ['id' => $id, 'name' => $name, 'admin' => $admin, 'superAdmin' => $superAdmin],
];
}

View File

@@ -27,7 +27,14 @@ use Psr\EventDispatcher\EventDispatcherInterface;
*/
final class WorkingTimeService
{
public function __construct(private TimesheetRepository $timesheetRepository, private WorkingTimeRepository $workingTimeRepository, private EventDispatcherInterface $eventDispatcher)
/** @var array<string, WorkingTime|null> */
private array $latestApprovals = [];
public function __construct(
private readonly TimesheetRepository $timesheetRepository,
private readonly WorkingTimeRepository $workingTimeRepository,
private readonly EventDispatcherInterface $eventDispatcher
)
{
}
@@ -43,7 +50,36 @@ final class WorkingTimeService
public function getLatestApproval(User $user): ?WorkingTime
{
return $this->workingTimeRepository->getLatestApproval($user);
if ($user->getId() === null) {
return null;
}
$key = 'u_' . $user->getId();
if (!\array_key_exists($key, $this->latestApprovals)) {
$this->latestApprovals[$key] = $this->workingTimeRepository->getLatestApproval($user);
}
return $this->latestApprovals[$key];
}
public function isApproved(User $user, \DateTimeInterface $dateTime): bool
{
$latestApproval = $this->getLatestApproval($user);
if ($latestApproval === null) {
return false;
}
$latestApprovalDate = $latestApproval->getDate();
$begin = \DateTimeImmutable::createFromInterface($dateTime);
$begin = $begin->setTime(0, 0, 0);
if ($begin > $latestApprovalDate) {
return false;
}
return true;
}
public function getYear(User $user, \DateTimeInterface $yearDate, \DateTimeInterface $until): Year
@@ -124,6 +160,11 @@ final class WorkingTimeService
$this->workingTimeRepository->persistScheduledWorkingTimes();
$key = 'u_' . $user->getId();
if (\array_key_exists($key, $this->latestApprovals)) {
unset($this->latestApprovals[$key]);
}
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approvedBy));
}

View File

@@ -281,20 +281,27 @@
{%- endblock secret_widget %}
{% block yearpicker_widget -%}
{% set yearInt = year|date_format('Y') %}
<div class="btn-group">
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousYear|report_date }}" data-event-bubbles="change">
{{ icon('left') }}
</a>
<a class="btn" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">
{% if show_range %}
{{ year|date_short }} &ndash; {{ nextYear|date_short }}
{% else %}
{{ year|date_format('Y') }}
{% endif %}
</span>
</a>
{% if show_range %}
<a class="btn" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">{{ year|date_short }} &ndash; {{ nextYear|date_short }}</span>
</a>
{% else %}
<button type="button" class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ yearInt }}
</button>
<ul class="dropdown-menu dropdown-menu-start pre-scrollable">
{% for i in (yearInt - 5)..(yearInt + 5) %}
{% set tmpYear = report_date(i) %}
<li><a href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ tmpYear|report_date }}" data-event-bubbles="change" class="dropdown-item">{{ tmpYear|date_format('Y') }}</a></li>
{% endfor %}
</ul>
{% endif %}
<a class="btn btn-right btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ nextYear|date_short }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ nextYear|report_date }}" data-event-bubbles="change">
{{ icon('right') }}

View File

@@ -1,8 +1,9 @@
{% macro init_frontend_loader() %}
{% set fdow = app.user is not null ? app.user.firstDayOfWeek : 'monday' %}
{% set configurations = javascript_configurations(app.user)|merge({
login: path('login'),
direction: tabler_bundle.rightToLeft ? 'rtl' : 'ltr',
first_dow_iso: iso_day_by_name(app.user.firstDayOfWeek),
first_dow_iso: iso_day_by_name(fdow),
}) %}
<script type="text/javascript">
window.addEventListener('load', function() {

View File

@@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'page_setup.html.twig' %}
{% block page_class %}quick-entry-page{% endblock %}
@@ -10,7 +10,6 @@
{% block box_before %}
{{ form_start(form, {attr: {id: 'quick-entries-form', class: 'form-dataTable quick-entries'}}) }}
{{ form_widget(form._token) }}
{{ form_widget(form.date) }}
{{ form_errors(form) }}
{% endblock %}
{% block box_after %}
@@ -107,10 +106,6 @@
};
recalculateTotals();
document.getElementById('{{ form.date.vars.id }}').addEventListener('change', function(ev) {
location.href = '{{ path('quick_entry', {'begin': '__BEGIN__'}) }}'.replace('__BEGIN__', ev.target.value);
});
document.getElementById('quick-entries-form').addEventListener('click', recalculateTotals);
document.getElementById('quick-entries-form').addEventListener('change', recalculateTotals);

View File

@@ -24,11 +24,13 @@ class QuickEntryControllerTest extends ControllerBaseTest
public function testIndexAction(): void
{
$client = $this->getClientForAuthenticatedUser();
$this->request($client, '/quick_entry');
$this->request($client, '/quick_entry/');
$this->assertTrue($client->getResponse()->isSuccessful());
$node = $client->getCrawler()->filter('section.content form[name=quick_entry_form]');
$node = $client->getCrawler()->filter('form[name=quick_entry_weekrange_form]');
self::assertEquals(1, $node->filter('div.btn-group.week-picker-btn-group')->count());
$node = $client->getCrawler()->filter('section.content form[name=quick_entry_form]');
self::assertEquals(1, $node->filter('input.btn-primary[type=submit]')->count());
$addBtn = $node->filter('button.btn-success[type=button]');
@@ -62,11 +64,10 @@ class QuickEntryControllerTest extends ControllerBaseTest
$fixture->setStartDate(new \DateTime('-7 days'));
$this->importFixture($fixture);
$this->request($client, '/quick_entry');
$this->request($client, '/quick_entry/');
$this->assertTrue($client->getResponse()->isSuccessful());
$node = $client->getCrawler()->filter('section.content form[name=quick_entry_form]');
self::assertEquals(1, $node->filter('div.btn-group.week-picker-btn-group')->count());
$node = $client->getCrawler()->filter('form[name=quick_entry_form]');
self::assertEquals(1, $node->filter('input.btn-primary[type=submit]')->count());
$addBtn = $node->filter('button.btn-success[type=button]');

View File

@@ -23,11 +23,12 @@ class QuickEntryModelTest extends TestCase
{
public function testEmptyModel(): void
{
$sut = new QuickEntryModel();
$user = new User();
$sut = new QuickEntryModel($user);
self::assertFalse($sut->isPrototype());
self::assertNull($sut->getProject());
self::assertNull($sut->getActivity());
self::assertNull($sut->getUser());
self::assertEquals($user, $sut->getUser());
self::assertEquals([], $sut->getNewTimesheet());
self::assertEquals([], $sut->getTimesheets());
self::assertNull($sut->getLatestEntry());
@@ -103,7 +104,8 @@ class QuickEntryModelTest extends TestCase
public function testHasExistingTimesheet(): void
{
$sut = new QuickEntryModel();
$user = new User();
$sut = new QuickEntryModel($user);
self::assertFalse($sut->hasExistingTimesheet());
$mock = $this->createMock(Timesheet::class);
@@ -128,11 +130,12 @@ class QuickEntryModelTest extends TestCase
public function testPrototype(): void
{
$sut = new QuickEntryModel();
$user = new User();
$sut = new QuickEntryModel($user);
$sut->markAsPrototype();
self::assertTrue($sut->isPrototype());
$sut = new QuickEntryModel();
$sut = new QuickEntryModel($user);
$sut->markAsPrototype();
$mock = $this->createMock(Timesheet::class);
$mock->method('getId')->willReturn(1);

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Model;
use App\Entity\User;
use App\Model\QuickEntryModel;
use App\Model\QuickEntryWeek;
use PHPUnit\Framework\TestCase;
@@ -27,7 +28,7 @@ class QuickEntryWeekTest extends TestCase
self::assertEquals([], $sut->getRows());
$rows = [
new QuickEntryModel()
new QuickEntryModel(new User())
];
$sut->setRows($rows);

View File

@@ -64,7 +64,7 @@ class ExtensionsTest extends TestCase
public function testGetFunctions(): void
{
$functions = ['class_name', 'iso_day_by_name', 'random_color'];
$functions = ['report_date', 'class_name', 'iso_day_by_name', 'random_color'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);

View File

@@ -483,6 +483,33 @@ class LocaleFormatExtensionsTest extends TestCase
self::assertEquals($expected, $sut->getJavascriptConfiguration($user));
}
public function testJavascriptConfigurationsForAnonymous(): void
{
$expected = [
'formatDuration' => '%h:%m',
'formatDate' => 'Y-m-d',
'defaultColor' => '#d2d6de',
'twentyFourHours' => false,
'updateBrowserTitle' => false,
'timezone' => 'Europe/Vienna',
'locale' => 'en',
'language' => 'en',
'user' => [
'id' => null,
'name' => 'anonymous',
'admin' => false,
'superAdmin' => false,
],
];
$security = $this->createMock(Security::class);
$sut = new LocaleFormatExtensions(new LocaleService($this->localeEn), $security);
$sut->setLocale('en');
self::assertEquals($expected, $sut->getJavascriptConfiguration());
}
public function testDurationDecimal(): void
{
$record = $this->getTimesheet(9437);

View File

@@ -12,6 +12,7 @@ namespace App\Tests\Validator\Constraints;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\QuickEntryModel as QuickEntryModelEntity;
use App\Validator\Constraints\QuickEntryModel;
use App\Validator\Constraints\QuickEntryModelValidator;
@@ -47,7 +48,8 @@ class QuickEntryModelValidatorTest extends ConstraintValidatorTestCase
public function testTriggersOnMissingProjectAndActivity(): void
{
$model = new QuickEntryModelEntity();
$user = new User();
$model = new QuickEntryModelEntity($user);
$timesheet = new Timesheet();
$timesheet->setBegin(new \DateTime());
$timesheet->setBegin(new \DateTime('+ 1 hour'));
@@ -66,7 +68,8 @@ class QuickEntryModelValidatorTest extends ConstraintValidatorTestCase
public function testTriggersOnMissingActivity(): void
{
$model = new QuickEntryModelEntity();
$user = new User();
$model = new QuickEntryModelEntity($user);
$model->setProject(new Project());
$timesheet = new Timesheet();
$timesheet->setBegin(new \DateTime());
@@ -83,7 +86,8 @@ class QuickEntryModelValidatorTest extends ConstraintValidatorTestCase
public function testTriggersOnMissingProject(): void
{
$model = new QuickEntryModelEntity();
$user = new User();
$model = new QuickEntryModelEntity($user);
$model->setActivity(new Activity());
$timesheet = new Timesheet();
$timesheet->setBegin(new \DateTime());
@@ -100,7 +104,8 @@ class QuickEntryModelValidatorTest extends ConstraintValidatorTestCase
public function testDoesNotTriggerOnPrototype(): void
{
$model = new QuickEntryModelEntity();
$user = new User();
$model = new QuickEntryModelEntity($user);
$this->validator->validate($model, new QuickEntryModel());
@@ -109,7 +114,8 @@ class QuickEntryModelValidatorTest extends ConstraintValidatorTestCase
public function testDoesNotTriggerOnProperlyFilled(): void
{
$model = new QuickEntryModelEntity();
$user = new User();
$model = new QuickEntryModelEntity($user);
$model->setActivity(new Activity());
$model->setProject(new Project());
$timesheet = new Timesheet();