Improve create and start permission handling (#613)
This commit is contained in:
@@ -272,7 +272,11 @@ $(function() {
|
||||
data: $form.serialize(),
|
||||
success: function(html) {
|
||||
btn.button('reset');
|
||||
if ($(html).find('#form_modal .modal-content .has-error').length > 0 || $(html).find(flashErrorIdentifier).length > 0) {
|
||||
var hasFieldError = $(html).find('#form_modal .modal-content .has-error').length > 0;
|
||||
var hasFormError = $(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0;
|
||||
var hasFlashError = $(html).find(flashErrorIdentifier).length > 0;
|
||||
|
||||
if (hasFieldError || hasFormError || hasFlashError) {
|
||||
$.kimai.ajaxFormInModal(html);
|
||||
} else {
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
|
||||
@@ -114,6 +114,7 @@ services:
|
||||
App\Validator\Constraints\TimesheetValidator:
|
||||
arguments:
|
||||
$ruleset: "%kimai.timesheet.rules%"
|
||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?22f3accd3baabc16310c",
|
||||
"build/app.css": "/build/app.css?d76b5fe68db00a63c65d6dbde19c6342",
|
||||
"build/app.js": "/build/app.js?039b40a3efd42cd2846a",
|
||||
"build/app.css": "/build/app.css?82e5f119685e8c17fbafec374d7a01e0",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"build/images/blue.png": "/build/images/blue.png?96f8a905",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
||||
|
||||
@@ -211,17 +211,6 @@ class TimesheetController extends BaseApiController
|
||||
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd()) {
|
||||
$this->repository->stopActiveEntries(
|
||||
$timesheet->getUser(),
|
||||
@@ -289,17 +278,6 @@ class TimesheetController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
@@ -97,18 +97,6 @@ trait TimesheetControllerTrait
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
if ($editForm->has('duration')) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $editForm->getData();
|
||||
$duration = $editForm->get('duration')->getData();
|
||||
$end = null;
|
||||
if ($duration > 0) {
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
}
|
||||
$record->setEnd($end);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
@@ -174,17 +162,6 @@ trait TimesheetControllerTrait
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
if ($createForm->has('duration')) {
|
||||
$duration = $createForm->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $createForm->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
try {
|
||||
|
||||
@@ -90,8 +90,10 @@ class Configuration implements ConfigurationInterface
|
||||
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
|
||||
if (class_exists($class)) {
|
||||
$rounding = new $class();
|
||||
|
||||
return !($rounding instanceof RoundingInterface);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Chosen rounding mode is invalid')
|
||||
|
||||
@@ -117,7 +117,38 @@ class TimesheetEditForm extends AbstractType
|
||||
if ($options['duration_only']) {
|
||||
$builder->add('duration', DurationType::class, [
|
||||
'required' => false,
|
||||
'docu_chapter' => 'timesheet.html#duration-format',
|
||||
'attr' => [
|
||||
'placeholder' => '00:00',
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::POST_SET_DATA,
|
||||
function (FormEvent $event) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
if (null === $data->getEnd()) {
|
||||
$event->getForm()->get('duration')->setData(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// make sure that duration is mapped back to end field
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
$duration = $data->getDuration();
|
||||
$end = null;
|
||||
if (null !== $duration) {
|
||||
$end = clone $data->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
}
|
||||
$data->setEnd($end);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$builder->add('end', DateTimePickerType::class, [
|
||||
'label' => 'label.end',
|
||||
@@ -270,7 +301,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'include_user' => false,
|
||||
'include_exported' => false,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet',
|
||||
'docu_chapter' => 'timesheet.html',
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -73,12 +73,18 @@ class DurationType extends AbstractType
|
||||
}
|
||||
},
|
||||
function ($formatToInt) use ($formatter, $pattern) {
|
||||
if (null === $formatToInt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($formatToInt)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!preg_match($pattern, $formatToInt)) {
|
||||
throw new TransformationFailedException('Invalid duration format given');
|
||||
}
|
||||
|
||||
try {
|
||||
return $formatter->parseDurationString($formatToInt);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -55,7 +55,7 @@ class InvoiceRendererType extends AbstractType
|
||||
return $choiceValue;
|
||||
},
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'docu_chapter' => 'invoices',
|
||||
'docu_chapter' => 'invoices.html',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
55
src/Security/CurrentUser.php
Normal file
55
src/Security/CurrentUser.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class CurrentUser
|
||||
{
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param TokenStorageInterface $storage
|
||||
* @param UserRepository $repository
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $storage, UserRepository $repository)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (null === $this->storage->getToken()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
if (!($user instanceof User)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->repository->getById($user->getId());
|
||||
}
|
||||
}
|
||||
@@ -33,5 +33,4 @@ interface RoundingInterface
|
||||
* @param $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes);
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserDateTimeFactory
|
||||
{
|
||||
@@ -20,18 +20,13 @@ class UserDateTimeFactory
|
||||
protected $timezone;
|
||||
|
||||
/**
|
||||
* @param TokenStorageInterface $tokenStorage
|
||||
* @param CurrentUser $user
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $tokenStorage)
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
if (null === $tokenStorage->getToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @var $user User */
|
||||
$user = $tokenStorage->getToken()->getUser();
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
$user = $user->getUser();
|
||||
if ($user instanceof User && null !== $user->getPreferenceValue('timezone')) {
|
||||
$timezone = $user->getPreferenceValue('timezone');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Utils\Duration;
|
||||
use App\Utils\LocaleSettings;
|
||||
@@ -121,6 +122,7 @@ class Extensions extends \Twig_Extension
|
||||
new TwigFilter('currency', [$this, 'currency']),
|
||||
new TwigFilter('country', [$this, 'country']),
|
||||
new TwigFilter('icon', [$this, 'icon']),
|
||||
new TwigFilter('docu_link', [$this, 'documentationLink']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -247,6 +249,15 @@ class Extensions extends \Twig_Extension
|
||||
return self::$icons[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function documentationLink($url = '')
|
||||
{
|
||||
return Constants::HOMEPAGE . '/documentation/' . $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $amount
|
||||
* @param string $currency
|
||||
|
||||
@@ -27,6 +27,7 @@ class Timesheet extends Constraint
|
||||
public const DISABLED_ACTIVITY_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d87';
|
||||
public const DISABLED_PROJECT_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d88';
|
||||
public const DISABLED_CUSTOMER_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d89';
|
||||
public const START_DISALLOWED = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d90';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
||||
@@ -38,6 +39,7 @@ class Timesheet extends Constraint
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
||||
self::START_DISALLOWED => 'You are not allowed to start this timesheet record.',
|
||||
];
|
||||
|
||||
public $message = 'This timesheet has invalid settings.';
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
@@ -18,17 +19,29 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class TimesheetValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $auth;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rules = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $durationOnly = false;
|
||||
|
||||
/**
|
||||
* @param AuthorizationCheckerInterface $auth
|
||||
* @param array $ruleset
|
||||
* @param bool $durationOnly
|
||||
*/
|
||||
public function __construct(array $ruleset)
|
||||
public function __construct(AuthorizationCheckerInterface $auth, array $ruleset, bool $durationOnly)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
$this->rules = $ruleset;
|
||||
$this->durationOnly = $durationOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +74,31 @@ class TimesheetValidator extends ConstraintValidator
|
||||
|
||||
$this->validateBeginAndEnd($value, $this->context);
|
||||
$this->validateActivityAndProject($value, $this->context);
|
||||
$this->validatePermissions($value, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validatePermissions(Timesheet $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
// special case that would otherwise need to be validated in several controllers:
|
||||
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
|
||||
// which might be disallowed for the current user
|
||||
if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) {
|
||||
if (!$this->auth->isGranted('start', $timesheet)) {
|
||||
$context->buildViolation('You are not allowed to start this timesheet record.')
|
||||
->atPath($this->durationOnly ? 'duration' : 'end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::START_DISALLOWED)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check active entries against hard_limit
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
eventRender: function(eventObj, $el) {
|
||||
if (eventObj.source.ajaxSettings.name !== 'kimaiUserTimeSource') {
|
||||
return;
|
||||
|
||||
}
|
||||
$el.popover({
|
||||
title: eventObj.title,
|
||||
@@ -98,17 +97,23 @@
|
||||
});
|
||||
},
|
||||
eventClick: function(calEvent, jsEvent, view) {
|
||||
{% if is_granted('edit_own_timesheet') %}
|
||||
location.href = '{{ path('timesheet_edit', {id: '-XX-'}) }}?origin=calendar'.replace('-XX-', calEvent.id);
|
||||
{% endif %}
|
||||
},
|
||||
dayClick: function(date, jsEvent, view) {
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
location.href = '{{ path('timesheet_create') }}' + '?origin=calendar&begin=' + date.format();
|
||||
{% endif %}
|
||||
},
|
||||
selectable: true,
|
||||
select: function( start, end, jsEvent, view) {
|
||||
if(view.type === 'month') {
|
||||
return;
|
||||
}
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
location.href = '{{ path('timesheet_create') }}' + '?from=' + start.format() + '&to=' + end.format();
|
||||
{% endif %}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h3 class="box-title">
|
||||
{{ title }}
|
||||
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
|
||||
<a href="{{ path('help_chapter', {'chapter': form.vars.docu_chapter}) }}"><i class="{{ 'help'|icon }}"></i></a>
|
||||
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block modal_title %}
|
||||
{{ title }}
|
||||
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
|
||||
<a href="{{ path('help_chapter', {'chapter': form.vars.docu_chapter}) }}"><i class="{{ 'help'|icon }}"></i></a>
|
||||
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block modal_body %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{# Adds the help icon, including a link to the documentation #}
|
||||
{% block form_label %}
|
||||
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
|
||||
<a href="{{ path('help_chapter', {'chapter': form.vars.docu_chapter}) }}"><i class="{{ 'help'|icon }}"></i></a>
|
||||
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock form_label %}
|
||||
|
||||
@@ -32,10 +32,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
<li class="footer"><a href="{{ path('timesheet_create') }}">{{ 'timesheet.start'|trans }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
{% elseif is_granted('create_own_timesheet') %}
|
||||
<li class="messages-menu">
|
||||
<a href="{{ path('timesheet_create') }}" class="ddt-large">
|
||||
<i class="{{ 'start'|icon }} fa-2x"></i>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if entries is defined and entries is not empty %}
|
||||
{% if entries is defined and entries is not empty and is_granted('start_own_timesheet') %}
|
||||
<li class="dropdown notifications-menu">
|
||||
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
|
||||
<i class="{{ 'activity'|icon }} fa-2x"></i>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ constant('App\\Constants::HOMEPAGE') }}/documentation/" target="_blank">
|
||||
<a href="{{ ''|docu_link }}" target="_blank">
|
||||
<div class="pull-left image">
|
||||
<i class="{{ 'help'|icon }} text-gray"></i>
|
||||
</div>
|
||||
|
||||
@@ -226,7 +226,6 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
if (count($validation) < 1) {
|
||||
// decorated form fields with icon have a different html structure, see kimai-theme.html.twig
|
||||
$classes = $field->parents()->getNode(1)->getAttribute('class');
|
||||
$this->assertContains('has-feedback', $classes, 'Form field has no validation message: ' . $name);
|
||||
$this->assertContains('has-error', $classes, 'Form field has no validation message: ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,9 +324,8 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$response = $client->getResponse();
|
||||
$this->assertTrue($response->isSuccessful());
|
||||
|
||||
$docuUrl = $this->createUrl('/help/timesheet');
|
||||
$this->assertContains(
|
||||
'<a href="' . $docuUrl . '"><i class="far fa-question-circle"></i></a>',
|
||||
'href="https://www.kimai.org/documentation/timesheet.html"',
|
||||
$response->getContent(),
|
||||
'Could not find link to documentation'
|
||||
);
|
||||
|
||||
@@ -40,8 +40,7 @@ abstract class AbstractEntityTest extends KernelTestCase
|
||||
foreach ($violations as $validation) {
|
||||
$violatedFields[$validation->getPropertyPath()] = $validation->getPropertyPath();
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, count($violatedFields), sprintf('Expected %s violations, found %s in %s.', $expected, $actual, implode(', ', array_keys($violatedFields))));
|
||||
$countViolations = count($violatedFields);
|
||||
|
||||
foreach ($fieldNames as $id => $propertyPath) {
|
||||
$foundField = false;
|
||||
@@ -54,6 +53,7 @@ abstract class AbstractEntityTest extends KernelTestCase
|
||||
}
|
||||
|
||||
$this->assertEmpty($violatedFields, sprintf('Unexpected violations found: %s', implode(', ', $violatedFields)));
|
||||
$this->assertEquals($expected, $countViolations, sprintf('Expected %s violations, found %s in %s.', $expected, $actual, implode(', ', array_keys($violatedFields))));
|
||||
}
|
||||
|
||||
protected function assertHasNoViolations($entity)
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Tests\Export\Renderer;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Export\Renderer\PDFRenderer;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
@@ -25,12 +27,17 @@ class PdfRendererTest extends AbstractRendererTest
|
||||
{
|
||||
protected function getDateTimeFactory()
|
||||
{
|
||||
$user = new User();
|
||||
$repository = $this->getMockBuilder(UserRepository::class)->setMethods(['getById'])->disableOriginalConstructor()->getMock();
|
||||
$repository->expects($this->once())->method('getById')->willReturn($user);
|
||||
$token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock();
|
||||
$token->expects($this->once())->method('getUser')->willReturn(new User());
|
||||
$token->expects($this->once())->method('getUser')->willReturn($user);
|
||||
$tokenStorage = new TokenStorage();
|
||||
$tokenStorage->setToken($token);
|
||||
|
||||
return new UserDateTimeFactory($tokenStorage);
|
||||
$user = new CurrentUser($tokenStorage, $repository);
|
||||
|
||||
return new UserDateTimeFactory($user);
|
||||
}
|
||||
|
||||
public function testConfiguration()
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Tests\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
@@ -24,18 +26,27 @@ class UserDateTimeFactoryTest extends TestCase
|
||||
public const TEST_TIMEZONE = 'Antarctica/DumontDUrville';
|
||||
|
||||
protected function createDateTimeFactory(string $timezone)
|
||||
{
|
||||
return new UserDateTimeFactory($this->getCurrentUserMock($timezone));
|
||||
}
|
||||
|
||||
protected function getCurrentUserMock($timezone = null)
|
||||
{
|
||||
$user = new User();
|
||||
$pref = new UserPreference();
|
||||
$pref->setName('timezone');
|
||||
$pref->setValue($timezone);
|
||||
$user->addPreference($pref);
|
||||
if (null !== $timezone) {
|
||||
$pref = new UserPreference();
|
||||
$pref->setName('timezone');
|
||||
$pref->setValue($timezone);
|
||||
$user->addPreference($pref);
|
||||
}
|
||||
$repository = $this->getMockBuilder(UserRepository::class)->setMethods(['getById'])->disableOriginalConstructor()->getMock();
|
||||
$repository->expects($this->once())->method('getById')->willReturn($user);
|
||||
$token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock();
|
||||
$token->expects($this->once())->method('getUser')->willReturn($user);
|
||||
$tokenStorage = new TokenStorage();
|
||||
$tokenStorage->setToken($token);
|
||||
|
||||
return new UserDateTimeFactory($tokenStorage);
|
||||
return new CurrentUser($tokenStorage, $repository);
|
||||
}
|
||||
|
||||
public function testGetTimezone()
|
||||
@@ -46,12 +57,14 @@ class UserDateTimeFactoryTest extends TestCase
|
||||
|
||||
public function testGetTimezoneWithFallbackTimezone()
|
||||
{
|
||||
$repository = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock();
|
||||
$token->expects($this->once())->method('getUser')->willReturn('anonymous');
|
||||
$tokenStorage = new TokenStorage();
|
||||
$tokenStorage->setToken($token);
|
||||
|
||||
$sut = new UserDateTimeFactory($tokenStorage);
|
||||
$current = new CurrentUser($tokenStorage, $repository);
|
||||
$sut = new UserDateTimeFactory($current);
|
||||
$this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName());
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class ExtensionsTest extends TestCase
|
||||
|
||||
public function testGetFilters()
|
||||
{
|
||||
$filters = ['duration', 'money', 'currency', 'country', 'icon'];
|
||||
$filters = ['duration', 'money', 'currency', 'country', 'icon', 'docu_link'];
|
||||
$sut = $this->getSut($this->localeDe);
|
||||
$twigFilters = $sut->getFilters();
|
||||
$this->assertCount(count($filters), $twigFilters);
|
||||
@@ -227,4 +227,21 @@ class ExtensionsTest extends TestCase
|
||||
$this->assertEquals('', $sut->icon('foo'));
|
||||
$this->assertEquals('bar', $sut->icon('foo', 'bar'));
|
||||
}
|
||||
|
||||
public function testDocuLink()
|
||||
{
|
||||
$data = [
|
||||
'timesheet.html' => 'https://www.kimai.org/documentation/timesheet.html',
|
||||
'timesheet.html#duration-format' => 'https://www.kimai.org/documentation/timesheet.html#duration-format',
|
||||
'invoice.html' => 'https://www.kimai.org/documentation/invoice.html',
|
||||
'' => 'https://www.kimai.org/documentation/',
|
||||
null => 'https://www.kimai.org/documentation/',
|
||||
];
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
foreach ($data as $input => $expected) {
|
||||
$result = $sut->documentationLink($input);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||
use App\Validator\Constraints\TimesheetValidator;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
@@ -23,13 +24,16 @@ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
*/
|
||||
class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
protected function createValidator()
|
||||
protected function createValidator($isGranted = true)
|
||||
{
|
||||
$options = [
|
||||
'allow_future_times' => false
|
||||
];
|
||||
|
||||
return new TimesheetValidator($options);
|
||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$authMock->method('isGranted')->willReturn($isGranted);
|
||||
|
||||
return new TimesheetValidator($authMock, $options, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +81,33 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
public function testRestartDisallowed()
|
||||
{
|
||||
$this->validator = $this->createValidator(false);
|
||||
$this->validator->initialize($this->context);
|
||||
|
||||
$begin = new \DateTime('-10 hour');
|
||||
$customer = new Customer();
|
||||
$activity = new Activity();
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$activity->setProject($project);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
->setBegin($begin)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
;
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
|
||||
|
||||
$this->buildViolation('You are not allowed to start this timesheet record.')
|
||||
->atPath('property.path.end')
|
||||
->setCode(TimesheetConstraint::START_DISALLOWED)
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
public function testEndBeforeBegin()
|
||||
{
|
||||
$end = new \DateTime('-10 hour');
|
||||
|
||||
Reference in New Issue
Block a user