Preparations for coming features (#1818)

* removed unused convert_tz code
* added timestampable listener to support automatic updates of change columns
* added modified_at, billable and category timesheet fields
* new timesheet API query parameter modified_after
This commit is contained in:
Kevin Papst
2020-07-13 23:35:06 +02:00
committed by GitHub
parent f2dd2331a5
commit f57b314466
18 changed files with 379 additions and 150 deletions

View File

@@ -3,7 +3,6 @@ doctrine:
dql: dql:
datetime_functions: datetime_functions:
addtime: DoctrineExtensions\Query\Mysql\AddTime addtime: DoctrineExtensions\Query\Mysql\AddTime
convert_tz: DoctrineExtensions\Query\Mysql\ConvertTz
date: DoctrineExtensions\Query\Mysql\Date date: DoctrineExtensions\Query\Mysql\Date
date_format: DoctrineExtensions\Query\Mysql\DateFormat date_format: DoctrineExtensions\Query\Mysql\DateFormat
dateadd: DoctrineExtensions\Query\Mysql\DateAdd dateadd: DoctrineExtensions\Query\Mysql\DateAdd

View File

@@ -2,7 +2,6 @@ doctrine:
orm: orm:
dql: dql:
datetime_functions: datetime_functions:
convert_tz: App\Doctrine\Query\Sqlite\ConvertTz
date: DoctrineExtensions\Query\Sqlite\Date date: DoctrineExtensions\Query\Sqlite\Date
date_format: DoctrineExtensions\Query\Sqlite\DateFormat date_format: DoctrineExtensions\Query\Sqlite\DateFormat
#dateadd: DoctrineExtensions\Query\Sqlite\DateAdd #dateadd: DoctrineExtensions\Query\Sqlite\DateAdd

View File

@@ -101,6 +101,14 @@ services:
tags: tags:
- { name: doctrine.event_subscriber, priority: 50 } - { name: doctrine.event_subscriber, priority: 50 }
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
Gedmo\Timestampable\TimestampableListener:
class: Gedmo\Timestampable\TimestampableListener
tags:
- { name: doctrine.event_subscriber, priority: 60 }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
# make sure, that sqlite supports foreign keys and cascade deletes # make sure, that sqlite supports foreign keys and cascade deletes
App\Doctrine\SqliteSessionInitSubscriber: App\Doctrine\SqliteSessionInitSubscriber:
class: App\Doctrine\SqliteSessionInitSubscriber class: App\Doctrine\SqliteSessionInitSubscriber

View File

@@ -145,6 +145,7 @@ class TimesheetController extends BaseApiController
* @Rest\QueryParam(name="active", requirements="0|1", strict=true, nullable=true, description="Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)") * @Rest\QueryParam(name="active", requirements="0|1", strict=true, nullable=true, description="Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)")
* @Rest\QueryParam(name="full", requirements="true", strict=true, nullable=true, description="Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)") * @Rest\QueryParam(name="full", requirements="true", strict=true, nullable=true, description="Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)")
* @Rest\QueryParam(name="term", description="Free search term") * @Rest\QueryParam(name="term", description="Free search term")
* @Rest\QueryParam(name="modified_after", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.")
* *
* @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')") * @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")
* *
@@ -255,6 +256,10 @@ class TimesheetController extends BaseApiController
$query->setSearchTerm(new SearchTerm($term)); $query->setSearchTerm(new SearchTerm($term));
} }
if (!empty($modifiedAfter = $paramFetcher->get('modified_after'))) {
$query->setModifiedAfter($this->dateTime->createDateTime($modifiedAfter));
}
/** @var Pagerfanta $data */ /** @var Pagerfanta $data */
$data = $this->repository->getPagerfantaForQuery($query); $data = $this->repository->getPagerfantaForQuery($query);
$data = (array) $data->getCurrentPageResults(); $data = (array) $data->getCurrentPageResults();

View File

@@ -1,57 +0,0 @@
<?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\Doctrine\Query\Sqlite;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
/**
* This is just a fake, as SQLITE does not support this by now.
*/
class ConvertTz extends FunctionNode
{
protected $dateExpression;
protected $fromTz;
protected $toTz;
/**
* {@inheritdoc}
*/
public function getSql(SqlWalker $sqlWalker)
{
$fieldName = $sqlWalker->walkArithmeticExpression($this->dateExpression);
$fromTz = $sqlWalker->walkStringPrimary($this->fromTz);
$toTz = $sqlWalker->walkStringPrimary($this->toTz);
return sprintf('%s', $fieldName);
}
/**
* {@inheritdoc}
*/
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->dateExpression = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->fromTz = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->toTz = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@@ -102,6 +102,7 @@ class Invoice
* @var string * @var string
* *
* @ORM\Column(name="currency", type="string", length=3, nullable=false) * @ORM\Column(name="currency", type="string", length=3, nullable=false)
* @Assert\NotNull()
* @Assert\Length(max=3) * @Assert\Length(max=3)
*/ */
private $currency; private $currency;
@@ -110,6 +111,7 @@ class Invoice
* @var int * @var int
* *
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false) * @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
* @Assert\NotNull()
* @Assert\Range(min = 0, max = 999) * @Assert\Range(min = 0, max = 999)
*/ */
private $dueDays = 30; private $dueDays = 30;
@@ -118,6 +120,7 @@ class Invoice
* @var float * @var float
* *
* @ORM\Column(name="vat", type="float", nullable=false) * @ORM\Column(name="vat", type="float", nullable=false)
* @Assert\NotNull()
* @Assert\Range(min = 0.0, max = 99.99) * @Assert\Range(min = 0.0, max = 99.99)
*/ */
private $vat = 0.00; private $vat = 0.00;
@@ -126,13 +129,16 @@ class Invoice
* @var string * @var string
* *
* @ORM\Column(name="status", type="string", length=20, nullable=false) * @ORM\Column(name="status", type="string", length=20, nullable=false)
* @Assert\NotNull()
*/ */
private $status = self::STATUS_NEW; private $status = self::STATUS_NEW;
/** /**
* @var string * @var string
* *
* @ORM\Column(name="invoice_filename", type="string", length=100, nullable=false) * @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false)
* @Assert\NotNull()
* @Assert\Length(min=1, max=150)
*/ */
private $invoiceFilename; private $invoiceFilename;

View File

@@ -15,6 +15,7 @@ use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -31,18 +32,29 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository") * @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
* @ORM\HasLifecycleCallbacks() * @ORM\HasLifecycleCallbacks()
* @App\Validator\Constraints\Timesheet * @App\Validator\Constraints\Timesheet
*
* columns={"user"} => IDX_4F60C6B18D93D649 => count results for user timesheets
* columns={"activity_id"} => IDX_4F60C6B181C06096 => ???
* columns={"user","start_time"} => IDX_4F60C6B18D93D649502DF587 => recent activities, user timesheet with date filzer
* columns={"start_time"} => IDX_4F60C6B1502DF587 => team timesheets with timerange filter only
* columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ???
* columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ???
*/ */
class Timesheet implements EntityWithMetaFields, ExportItemInterface class Timesheet implements EntityWithMetaFields, ExportItemInterface
{ {
public const TYPE_TIMESHEET = 'timesheet'; /**
public const CATEGORY_WORK = 'work'; * Category: Normal work-time (default category)
*/
public const WORK = 'work';
/**
* Category: Holiday
*/
public const HOLIDAY = 'holiday';
/**
* Category: Sickness
*/
public const SICKNESS = 'sickness';
/**
* Category: Parental leave
*/
public const PARENTAL = 'parental';
/**
* Category: Overtime reduction
*/
public const OVERTIME = 'overtime';
/** /**
* @var int|null * @var int|null
@@ -161,6 +173,30 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
*/ */
private $exported = false; private $exported = false;
/**
* @var bool
*
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
* @Assert\NotNull()
*/
private $billable = true;
/**
* @var string
*
* @ORM\Column(name="category", type="string", length=10, nullable=false, options={"default": "work"})
* @Assert\NotNull()
*/
private $category = self::WORK;
/**
* @var DateTime|null
*
* @Gedmo\Timestampable
* @ORM\Column(name="modified_at", type="datetime", nullable=true)
*/
private $modifiedAt;
/** /**
* @var Tag[]|ArrayCollection * @var Tag[]|ArrayCollection
* *
@@ -185,9 +221,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
*/ */
private $meta; private $meta;
/**
* Default constructor, initializes collections
*/
public function __construct() public function __construct()
{ {
$this->tags = new ArrayCollection(); $this->tags = new ArrayCollection();
@@ -472,16 +505,44 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
return $this; return $this;
} }
/**
* This method returns ALWAYS: "timesheet"
*
* @return string
*/
public function getType(): string public function getType(): string
{ {
// this will be improved in a future version return 'timesheet';
return self::TYPE_TIMESHEET;
} }
public function getCategory(): string public function getCategory(): string
{ {
// this will be improved in a future version return $this->category;
return self::CATEGORY_WORK; }
public function setCategory(string $category): Timesheet
{
$allowed = [self::WORK, self::HOLIDAY, self::SICKNESS, self::PARENTAL, self::OVERTIME];
if (!\in_array($category, $allowed)) {
throw new \InvalidArgumentException(sprintf('Invalid timesheet category "%s" given, expected one of: %s', $category, implode(', ', $allowed)));
}
$this->category = $category;
return $this;
}
public function isBillable(): bool
{
return $this->billable;
}
public function setBillable(bool $billable): Timesheet
{
$this->billable = $billable;
return $this;
} }
public function getFixedRate(): ?float public function getFixedRate(): ?float
@@ -508,6 +569,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
return $this; return $this;
} }
public function getModifiedAt(): ?DateTime
{
return $this->modifiedAt;
}
/** /**
* @return Collection|MetaTableTypeInterface[] * @return Collection|MetaTableTypeInterface[]
*/ */

View File

@@ -52,9 +52,11 @@ class UserEnvironmentSubscriber implements EventSubscriberInterface
$user = $this->storage->getToken()->getUser(); $user = $this->storage->getToken()->getUser();
// the locale depends on the request, not on the user configuration
\Locale::setDefault($event->getRequest()->getLocale());
if ($user instanceof User) { if ($user instanceof User) {
date_default_timezone_set($user->getTimezone()); date_default_timezone_set($user->getTimezone());
\Locale::setDefault($user->getLocale());
$user->initCanSeeAllData($this->auth->isGranted('view_all_data')); $user->initCanSeeAllData($this->auth->isGranted('view_all_data'));
} }
} }

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* 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 DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 1.10
*/
final class Version20200705152310 extends AbstractMigration
{
public function getDescription(): string
{
return 'Updated invoice and added timesheet columns';
}
public function up(Schema $schema): void
{
$invoices = $schema->getTable('kimai2_invoices');
$invoices->getColumn('invoice_filename')->setLength(150);
$timesheet = $schema->getTable('kimai2_timesheet');
$timesheet->addColumn('billable', 'boolean', ['notnull' => false, 'default' => true]);
$timesheet->addColumn('category', 'string', ['length' => 10, 'notnull' => true, 'default' => 'work']);
$timesheet->addColumn('modified_at', 'datetime', ['notnull' => false]);
}
public function down(Schema $schema): void
{
$invoices = $schema->getTable('kimai2_invoices');
$invoices->getColumn('invoice_filename')->setLength(100);
$timesheet = $schema->getTable('kimai2_timesheet');
$timesheet->dropColumn('billable');
$timesheet->dropColumn('category');
$timesheet->dropColumn('modified_at');
}
protected function isSupportingForeignKeys(): bool
{
return false;
}
public function isTransactional(): bool
{
if ($this->isPlatformSqlite()) {
// does fail if we use transactions, as tables are re-created and foreign keys would fail
return false;
}
return true;
}
}

View File

@@ -22,6 +22,12 @@ class InvoiceQuery extends TimesheetQuery
*/ */
private $markAsExported = false; private $markAsExported = false;
public function __construct()
{
parent::__construct();
$this->setBillable(InvoiceQuery::STATE_BILLABLE);
}
public function getTemplate(): ?InvoiceTemplate public function getTemplate(): ?InvoiceTemplate
{ {
return $this->template; return $this->template;

View File

@@ -24,6 +24,8 @@ class TimesheetQuery extends ActivityQuery
public const STATE_STOPPED = 3; public const STATE_STOPPED = 3;
public const STATE_EXPORTED = 4; public const STATE_EXPORTED = 4;
public const STATE_NOT_EXPORTED = 5; public const STATE_NOT_EXPORTED = 5;
public const STATE_BILLABLE = 6;
public const STATE_NOT_BILLABLE = 7;
public const TIMESHEET_ORDER_ALLOWED = ['begin', 'end', 'duration', 'rate', 'customer', 'project', 'activity', 'description']; public const TIMESHEET_ORDER_ALLOWED = ['begin', 'end', 'duration', 'rate', 'customer', 'project', 'activity', 'description'];
@@ -43,6 +45,14 @@ class TimesheetQuery extends ActivityQuery
* @var int * @var int
*/ */
protected $exported = self::STATE_ALL; protected $exported = self::STATE_ALL;
/**
* @var int
*/
private $billable = self::STATE_ALL;
/**
* @var \DateTime|null
*/
private $modifiedAfter;
/** /**
* @var DateRange * @var DateRange
*/ */
@@ -115,7 +125,7 @@ class TimesheetQuery extends ActivityQuery
/** /**
* @return Activity|int|null * @return Activity|int|null
* @deprecated since 1.9 - use getProjects() instead - will be removed with 2.0 * @deprecated since 1.9 - use getActivities() instead - will be removed with 2.0
*/ */
public function getActivity() public function getActivity()
{ {
@@ -151,7 +161,7 @@ class TimesheetQuery extends ActivityQuery
* @param Activity|int $activity * @param Activity|int $activity
* @return $this * @return $this
*/ */
public function addActivity($activity) public function addActivity($activity): TimesheetQuery
{ {
$this->activities[] = $activity; $this->activities[] = $activity;
@@ -162,7 +172,7 @@ class TimesheetQuery extends ActivityQuery
* @param Activity[]|int[] $activities * @param Activity[]|int[] $activities
* @return $this * @return $this
*/ */
public function setActivities(array $activities) public function setActivities(array $activities): TimesheetQuery
{ {
$this->activities = $activities; $this->activities = $activities;
@@ -174,19 +184,22 @@ class TimesheetQuery extends ActivityQuery
return !empty($this->activities); return !empty($this->activities);
} }
/** public function getState(): int
* @return int
*/
public function getState()
{ {
return $this->state; return $this->state;
} }
/** public function isRunning(): bool
* @param int $state {
* @return TimesheetQuery return $this->state === self::STATE_RUNNING;
*/ }
public function setState($state)
public function isStopped(): bool
{
return $this->state === self::STATE_STOPPED;
}
public function setState(int $state): TimesheetQuery
{ {
$state = (int) $state; $state = (int) $state;
if (\in_array($state, [self::STATE_ALL, self::STATE_RUNNING, self::STATE_STOPPED], true)) { if (\in_array($state, [self::STATE_ALL, self::STATE_RUNNING, self::STATE_STOPPED], true)) {
@@ -196,19 +209,22 @@ class TimesheetQuery extends ActivityQuery
return $this; return $this;
} }
/** public function getExported(): int
* @return int
*/
public function getExported()
{ {
return $this->exported; return $this->exported;
} }
/** public function isExported(): bool
* @param int $exported {
* @return TimesheetQuery return $this->exported === self::STATE_EXPORTED;
*/ }
public function setExported($exported)
public function isNotExported(): bool
{
return $this->exported === self::STATE_NOT_EXPORTED;
}
public function setExported(int $exported): TimesheetQuery
{ {
$exported = (int) $exported; $exported = (int) $exported;
if (\in_array($exported, [self::STATE_ALL, self::STATE_EXPORTED, self::STATE_NOT_EXPORTED], true)) { if (\in_array($exported, [self::STATE_ALL, self::STATE_EXPORTED, self::STATE_NOT_EXPORTED], true)) {
@@ -223,11 +239,7 @@ class TimesheetQuery extends ActivityQuery
return $this->dateRange->getBegin(); return $this->dateRange->getBegin();
} }
/** public function setBegin(\DateTime $begin): TimesheetQuery
* @param \DateTime $begin
* @return TimesheetQuery
*/
public function setBegin($begin)
{ {
$this->dateRange->setBegin($begin); $this->dateRange->setBegin($begin);
@@ -239,40 +251,26 @@ class TimesheetQuery extends ActivityQuery
return $this->dateRange->getEnd(); return $this->dateRange->getEnd();
} }
/** public function setEnd(\DateTime $end): TimesheetQuery
* @param \DateTime $end
* @return TimesheetQuery
*/
public function setEnd($end)
{ {
$this->dateRange->setEnd($end); $this->dateRange->setEnd($end);
return $this; return $this;
} }
/**
* @return DateRange
*/
public function getDateRange(): DateRange public function getDateRange(): DateRange
{ {
return $this->dateRange; return $this->dateRange;
} }
/** public function setDateRange(DateRange $dateRange): TimesheetQuery
* @param DateRange $dateRange
* @return TimesheetQuery
*/
public function setDateRange(DateRange $dateRange)
{ {
$this->dateRange = $dateRange; $this->dateRange = $dateRange;
return $this; return $this;
} }
/** public function getTags(bool $allowUnknown = false): iterable
* @return iterable
*/
public function getTags($allowUnknown = false)
{ {
if (empty($this->tags)) { if (empty($this->tags)) {
return []; return [];
@@ -290,14 +288,46 @@ class TimesheetQuery extends ActivityQuery
return $result; return $result;
} }
/** public function setTags(iterable $tags): TimesheetQuery
* @param iterable $tags
* @return $this
*/
public function setTags(iterable $tags)
{ {
$this->tags = $tags; $this->tags = $tags;
return $this; return $this;
} }
public function getBillable(): int
{
return $this->billable;
}
public function isBillable(): bool
{
return $this->billable === self::STATE_BILLABLE;
}
public function isNotBillable(): bool
{
return $this->billable === self::STATE_NOT_BILLABLE;
}
public function setBillable(int $billable): TimesheetQuery
{
if (\in_array($billable, [self::STATE_ALL, self::STATE_BILLABLE, self::STATE_NOT_BILLABLE], true)) {
$this->billable = $billable;
}
return $this;
}
public function getModifiedAfter(): ?\DateTime
{
return $this->modifiedAfter;
}
public function setModifiedAfter(\DateTime $modifiedAfter): TimesheetQuery
{
$this->modifiedAfter = $modifiedAfter;
return $this;
}
} }

View File

@@ -753,9 +753,9 @@ class TimesheetRepository extends EntityRepository
->setParameter('begin', $query->getBegin()); ->setParameter('begin', $query->getBegin());
} }
if (TimesheetQuery::STATE_RUNNING == $query->getState()) { if ($query->isRunning()) {
$qb->andWhere($qb->expr()->isNull('t.end')); $qb->andWhere($qb->expr()->isNull('t.end'));
} elseif (TimesheetQuery::STATE_STOPPED == $query->getState()) { } elseif ($query->isStopped()) {
$qb->andWhere($qb->expr()->isNotNull('t.end')); $qb->andWhere($qb->expr()->isNotNull('t.end'));
} }
@@ -764,12 +764,23 @@ class TimesheetRepository extends EntityRepository
->setParameter('end', $query->getEnd()); ->setParameter('end', $query->getEnd());
} }
if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) { if ($query->isExported()) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, \PDO::PARAM_BOOL); $qb->andWhere('t.exported = :exported')->setParameter('exported', true, \PDO::PARAM_BOOL);
} elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) { } elseif ($query->isNotExported()) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, \PDO::PARAM_BOOL); $qb->andWhere('t.exported = :exported')->setParameter('exported', false, \PDO::PARAM_BOOL);
} }
if ($query->isBillable()) {
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, \PDO::PARAM_BOOL);
} elseif ($query->isNotBillable()) {
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, \PDO::PARAM_BOOL);
}
if (null !== $query->getModifiedAfter()) {
$qb->andWhere($qb->expr()->gte($this->getDatetimeFieldSql('t.modifiedAt'), ':modified_at'))
->setParameter('modified_at', $query->getModifiedAfter());
}
if ($query->hasActivities()) { if ($query->hasActivities()) {
$qb->andWhere($qb->expr()->in('t.activity', ':activity')) $qb->andWhere($qb->expr()->in('t.activity', ':activity'))
->setParameter('activity', $query->getActivities()); ->setParameter('activity', $query->getActivities());

View File

@@ -16,7 +16,6 @@ use App\Utils\Duration;
use App\Utils\LocaleFormats; use App\Utils\LocaleFormats;
use App\Utils\LocaleHelper; use App\Utils\LocaleHelper;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Languages;
use Symfony\Component\Intl\Locales; use Symfony\Component\Intl\Locales;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
@@ -163,6 +162,8 @@ final class LocaleExtensions extends AbstractExtension
} }
/** /**
* Returns the currency symbol.
*
* @param string $currency * @param string $currency
* @return string * @return string
*/ */

View File

@@ -141,6 +141,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionWithQuery() public function testGetCollectionWithQuery()
{ {
$modifiedAfter = new \DateTime('-1 hour');
$begin = new \DateTime('first day of this month'); $begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0); $begin->setTime(0, 0, 0);
$end = new \DateTime('last day of this month'); $end = new \DateTime('last day of this month');
@@ -155,6 +156,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
'order' => 'DESC', 'order' => 'DESC',
'orderBy' => 'rate', 'orderBy' => 'rate',
'active' => 0, 'active' => 0,
'modified_after' => $modifiedAfter->format(self::DATE_FORMAT_HTML5),
'begin' => $begin->format(self::DATE_FORMAT_HTML5), 'begin' => $begin->format(self::DATE_FORMAT_HTML5),
'end' => $end->format(self::DATE_FORMAT_HTML5), 'end' => $end->format(self::DATE_FORMAT_HTML5),
'exported' => 0, 'exported' => 0,

View File

@@ -33,6 +33,8 @@ class TimesheetTest extends TestCase
self::assertNull($sut->getId()); self::assertNull($sut->getId());
self::assertNull($sut->getBegin()); self::assertNull($sut->getBegin());
self::assertNull($sut->getEnd()); self::assertNull($sut->getEnd());
self::assertTrue($sut->isBillable());
self::assertNull($sut->getModifiedAt());
self::assertSame(0, $sut->getDuration()); self::assertSame(0, $sut->getDuration());
self::assertNull($sut->getUser()); self::assertNull($sut->getUser());
self::assertNull($sut->getActivity()); self::assertNull($sut->getActivity());
@@ -140,4 +142,34 @@ class TimesheetTest extends TestCase
self::assertEquals(3, $sut->getMetaFields()->count()); self::assertEquals(3, $sut->getMetaFields()->count());
self::assertCount(2, $sut->getVisibleMetaFields()); self::assertCount(2, $sut->getVisibleMetaFields());
} }
public function testBillable()
{
$sut = new Timesheet();
self::assertTrue($sut->isBillable());
self::assertInstanceOf(Timesheet::class, $sut->setBillable(false));
self::assertFalse($sut->isBillable());
self::assertInstanceOf(Timesheet::class, $sut->setBillable(true));
self::assertTrue($sut->isBillable());
}
public function testCategory()
{
$sut = new Timesheet();
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::HOLIDAY));
self::assertEquals('holiday', $sut->getCategory());
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::WORK));
self::assertEquals('work', $sut->getCategory());
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::SICKNESS));
self::assertEquals('sickness', $sut->getCategory());
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::PARENTAL));
self::assertEquals('parental', $sut->getCategory());
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::OVERTIME));
self::assertEquals('overtime', $sut->getCategory());
self::expectException(\InvalidArgumentException::class);
self::expectExceptionMessage('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
$sut->setCategory('foo');
}
} }

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Repository\Query; namespace App\Tests\Repository\Query;
use App\Repository\Query\InvoiceQuery; use App\Repository\Query\InvoiceQuery;
use App\Repository\Query\TimesheetQuery;
/** /**
* @covers \App\Repository\Query\InvoiceQuery * @covers \App\Repository\Query\InvoiceQuery
@@ -33,13 +34,19 @@ class InvoiceQueryTest extends TimesheetQueryTest
$this->assertState($sut); $this->assertState($sut);
$this->assertExported($sut); $this->assertExported($sut);
$this->assertMarkAsExported($sut); $this->assertMarkAsExported($sut);
$this->assertModifiedAfter($sut);
self::assertEquals(TimesheetQuery::STATE_BILLABLE, $sut->getBillable());
self::assertTrue($sut->isBillable());
self::assertFalse($sut->isNotBillable());
$this->assertBillable($sut);
} }
protected function assertMarkAsExported(InvoiceQuery $sut) protected function assertMarkAsExported(InvoiceQuery $sut)
{ {
$this->assertFalse($sut->isMarkAsExported()); self::assertFalse($sut->isMarkAsExported());
$sut->setMarkAsExported(true); $sut->setMarkAsExported(true);
$this->assertTrue($sut->isMarkAsExported()); self::assertTrue($sut->isMarkAsExported());
} }
} }

View File

@@ -35,23 +35,29 @@ class TimesheetQueryTest extends BaseQueryTest
$this->assertState($sut); $this->assertState($sut);
$this->assertExported($sut); $this->assertExported($sut);
$this->assertSearchTerm($sut); $this->assertSearchTerm($sut);
$this->assertModifiedAfter($sut);
self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable());
self::assertFalse($sut->isBillable());
self::assertFalse($sut->isNotBillable());
$this->assertBillable($sut);
$this->assertResetByFormError(new TimesheetQuery(), 'begin', 'DESC'); $this->assertResetByFormError(new TimesheetQuery(), 'begin', 'DESC');
} }
protected function assertUser(TimesheetQuery $sut) protected function assertUser(TimesheetQuery $sut)
{ {
$this->assertNull($sut->getUser()); self::assertNull($sut->getUser());
$expected = new User(); $expected = new User();
$expected->setUsername('foo-bar'); $expected->setUsername('foo-bar');
$sut->setUser($expected); self::assertInstanceOf(TimesheetQuery::class, $sut->setUser($expected));
$this->assertEquals($expected, $sut->getUser()); self::assertEquals($expected, $sut->getUser());
} }
protected function assertUsers(TimesheetQuery $sut) protected function assertUsers(TimesheetQuery $sut)
{ {
$this->assertEmpty($sut->getUsers()); self::assertEmpty($sut->getUsers());
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock(); $user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1); $user->method('getId')->willReturn(1);
@@ -70,43 +76,86 @@ class TimesheetQueryTest extends BaseQueryTest
$sut->addUser($user); $sut->addUser($user);
$sut->removeUser($user); $sut->removeUser($user);
$this->assertCount(2, $sut->getUsers()); self::assertCount(2, $sut->getUsers());
} }
protected function assertState(TimesheetQuery $sut) protected function assertState(TimesheetQuery $sut)
{ {
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
self::assertFalse($sut->isRunning());
self::assertFalse($sut->isStopped());
$sut->setState(PHP_INT_MAX); self::assertInstanceOf(TimesheetQuery::class, $sut->setState(PHP_INT_MAX));
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
$sut->setState(TimesheetQuery::STATE_STOPPED); $sut->setState(TimesheetQuery::STATE_STOPPED);
$this->assertEquals(TimesheetQuery::STATE_STOPPED, $sut->getState()); self::assertEquals(TimesheetQuery::STATE_STOPPED, $sut->getState());
self::assertFalse($sut->isRunning());
self::assertTrue($sut->isStopped());
$sut->setState(TimesheetQuery::STATE_RUNNING); $sut->setState(TimesheetQuery::STATE_RUNNING);
$this->assertEquals(TimesheetQuery::STATE_RUNNING, $sut->getState()); self::assertEquals(TimesheetQuery::STATE_RUNNING, $sut->getState());
self::assertTrue($sut->isRunning());
self::assertFalse($sut->isStopped());
$sut->setState(TimesheetQuery::STATE_ALL); $sut->setState(TimesheetQuery::STATE_ALL);
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
} }
protected function assertExported(TimesheetQuery $sut) protected function assertExported(TimesheetQuery $sut)
{ {
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
self::assertFalse($sut->isExported());
self::assertFalse($sut->isNotExported());
$sut->setExported(PHP_INT_MAX); self::assertInstanceOf(TimesheetQuery::class, $sut->setExported(PHP_INT_MAX));
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
$sut->setExported(TimesheetQuery::STATE_EXPORTED); $sut->setExported(TimesheetQuery::STATE_EXPORTED);
$this->assertEquals(TimesheetQuery::STATE_EXPORTED, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_EXPORTED, $sut->getExported());
self::assertTrue($sut->isExported());
self::assertFalse($sut->isNotExported());
$sut->setExported(TimesheetQuery::STATE_NOT_EXPORTED); $sut->setExported(TimesheetQuery::STATE_NOT_EXPORTED);
$this->assertEquals(TimesheetQuery::STATE_NOT_EXPORTED, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_NOT_EXPORTED, $sut->getExported());
self::assertFalse($sut->isExported());
self::assertTrue($sut->isNotExported());
$sut->setExported(TimesheetQuery::STATE_ALL); $sut->setExported(TimesheetQuery::STATE_ALL);
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
$sut->setExported('02'); $sut->setExported('02');
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
}
protected function assertModifiedAfter(TimesheetQuery $sut)
{
self::assertNull($sut->getModifiedAfter());
$date = new \DateTime('-3 hours');
self::assertInstanceOf(TimesheetQuery::class, $sut->setModifiedAfter($date));
self::assertNotNull($sut->getModifiedAfter()); // just here to fix a PHPStan issue
self::assertSame($date, $sut->getModifiedAfter());
}
protected function assertBillable(TimesheetQuery $sut)
{
self::assertInstanceOf(TimesheetQuery::class, $sut->setBillable(TimesheetQuery::STATE_ALL));
self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable());
self::assertFalse($sut->isBillable());
self::assertFalse($sut->isNotBillable());
$sut->setBillable(PHP_INT_MAX);
self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable());
$sut->setBillable(TimesheetQuery::STATE_BILLABLE);
self::assertEquals(TimesheetQuery::STATE_BILLABLE, $sut->getBillable());
self::assertTrue($sut->isBillable());
self::assertFalse($sut->isNotBillable());
$sut->setBillable(TimesheetQuery::STATE_NOT_BILLABLE);
self::assertEquals(TimesheetQuery::STATE_NOT_BILLABLE, $sut->getBillable());
self::assertFalse($sut->isBillable());
self::assertTrue($sut->isNotBillable());
} }
} }

Binary file not shown.