diff --git a/UPGRADING.md b/UPGRADING.md index ae8a2609..1d42042f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -20,9 +20,16 @@ And make sure to **create a backup before you start**. ## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9) -**BC BREAK** - in an ongoing effort to simplify future installation and upgrade processes the `.env` variable `DATABASE_PREFIX` was removed. +Follow the normal update and database migration process (see above). + +Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)! + +**BC BREAKS** +- in an ongoing effort to simplify future installation and upgrade processes the `.env` variable `DATABASE_PREFIX` was removed. The table prefix is now hardcoded to `kimai2_`. If you used another prefix, you have to rename your tables manually before starting the update process. +- API: DateTime objects will be returned including timezone identifier (previously 2019-03-02 14:23 - now 2019-03-02T14:23:00+00:00) + ## [0.8.1](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1) diff --git a/config/bundles.php b/config/bundles.php index 9a367525..fe946c9a 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -18,8 +18,8 @@ return [ Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true], + JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], FOS\UserBundle\FOSUserBundle::class => ['all' => true], FOS\RestBundle\FOSRestBundle::class => ['all' => true], Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], - JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], ]; diff --git a/config/packages/jms_serializer.yaml b/config/packages/jms_serializer.yaml index 1fe047cf..33abf699 100644 --- a/config/packages/jms_serializer.yaml +++ b/config/packages/jms_serializer.yaml @@ -1,7 +1,4 @@ jms_serializer: - handlers: - datetime: - default_format: "Y-m-d H:i" visitors: xml: format_output: '%kernel.debug%' diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index fb1a876b..dc18a0cd 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -8,6 +8,7 @@ nelmio_api_doc: - { alias: TimesheetEditForm, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] } - { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] } - { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] } + - { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] } areas: path_patterns: - ^/api(?!/doc) @@ -16,7 +17,7 @@ nelmio_api_doc: schemes: [http, https] info: title: Kimai 2 - API Docs - description: REST API description for the Kimai 2 time-tracking software. Do not rely on the example values/models, they are currently generated wrong. Please fetch the request and response structure from the API itself. + description: REST API for the Kimai 2 time-tracking software. It's rather limited by now. If you need other methods, please let me know at GitHub! version: 0.2 # parameters: # hostname: diff --git a/config/serializer/App/API.Model.I18n.yml b/config/serializer/App/API.Model.I18n.yml new file mode 100644 index 00000000..a3920c83 --- /dev/null +++ b/config/serializer/App/API.Model.I18n.yml @@ -0,0 +1,32 @@ +App\API\Model\I18n: + exclusion_policy: All + custom_accessor_order: [formDateTime, formDate, dateTime, date, time, duration, is24hours] + properties: + formDateTime: + include: true + type: string + groups: [Default] + formDate: + include: true + type: string + groups: [Default] + dateTime: + include: true + type: string + groups: [Default] + time: + include: true + type: string + groups: [Default] + date: + include: true + type: string + groups: [Default] + duration: + include: true + type: string + groups: [Default] + is24hours: + include: true + type: boolean + groups: [Default] diff --git a/config/serializer/FOS/UserBundle/Model.User.yml b/config/serializer/FOS/UserBundle/Model.User.yml index 7382a506..2ca9689a 100644 --- a/config/serializer/FOS/UserBundle/Model.User.yml +++ b/config/serializer/FOS/UserBundle/Model.User.yml @@ -8,6 +8,7 @@ FOS\UserBundle\Model\User: include: true groups: [Entity] roles: + type: array include: true groups: [Entity] email: diff --git a/config/services.yaml b/config/services.yaml index 59341d2e..11e70cae 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,7 +20,7 @@ services: # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Constants.php}' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -55,7 +55,7 @@ services: arguments: $defaults: "%kimai.defaults%" - App\Utils\LocaleSettings: + App\Configuration\LanguageFormattings: arguments: $languageSettings: "%kimai.languages%" diff --git a/src/API/ConfigurationController.php b/src/API/ConfigurationController.php new file mode 100644 index 00000000..93cd093c --- /dev/null +++ b/src/API/ConfigurationController.php @@ -0,0 +1,75 @@ +viewHandler = $viewHandler; + $this->formats = $formats; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the locale specific configurations for this user", + * @SWG\Schema(ref="#/definitions/I18nConfig") + * ) + * + * @Rest\Get(path="/config/i18n") + */ + public function i18nAction() + { + /** @var User $user */ + $user = $this->getUser(); + $locale = $user->getLocale(); + + $model = new I18n(); + $model + ->setFormDateTime($this->formats->getDateTimeTypeFormat($locale)) + ->setFormDate($this->formats->getDateTypeFormat($locale)) + ->setDateTime($this->formats->getDateTimeFormat($locale)) + ->setDate($this->formats->getDateFormat($locale)) + ->setDuration($this->formats->getDurationFormat($locale)) + ->setTime($this->formats->getTimeFormat($locale)) + ->setIs24hours($this->formats->isTwentyFourHours($locale)) + ; + + $view = new View($model, 200); + $view->getContext()->setGroups(['Default', 'Config']); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/Model/I18n.php b/src/API/Model/I18n.php new file mode 100644 index 00000000..4ddfd63d --- /dev/null +++ b/src/API/Model/I18n.php @@ -0,0 +1,190 @@ +formDateTime; + } + + /** + * @param string $formDateTime + * @return I18n + */ + public function setFormDateTime(string $formDateTime) + { + $this->formDateTime = $formDateTime; + + return $this; + } + + /** + * @return string + */ + public function getFormDate(): string + { + return $this->formDate; + } + + /** + * @param string $formDate + * @return I18n + */ + public function setFormDate(string $formDate) + { + $this->formDate = $formDate; + + return $this; + } + + /** + * @return string + */ + public function getDateTime(): string + { + return $this->dateTime; + } + + /** + * @param string $dateTime + * @return I18n + */ + public function setDateTime(string $dateTime) + { + $this->dateTime = $dateTime; + + return $this; + } + + /** + * @return string + */ + public function getDate(): string + { + return $this->date; + } + + /** + * @param string $date + * @return I18n + */ + public function setDate(string $date) + { + $this->date = $date; + + return $this; + } + + /** + * @return string + */ + public function getDuration(): string + { + return $this->duration; + } + + /** + * @param string $duration + * @return I18n + */ + public function setDuration(string $duration) + { + $this->duration = $duration; + + return $this; + } + + /** + * @return string + */ + public function getTime(): string + { + return $this->time; + } + + /** + * @param string $time + * @return I18n + */ + public function setTime(string $time) + { + $this->time = $time; + + return $this; + } + + /** + * @return bool + */ + public function isIs24hours(): bool + { + return $this->is24hours; + } + + /** + * @param bool $is24hours + * @return I18n + */ + public function setIs24hours(bool $is24hours) + { + $this->is24hours = $is24hours; + + return $this; + } +} diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 77e21c46..aae9dc19 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -26,6 +26,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Swagger\Annotations as SWG; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints; /** * @RouteResource("Timesheet") @@ -84,6 +85,9 @@ class TimesheetController extends BaseApiController * @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 25)") * @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')") * @Rest\QueryParam(name="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered (allowed values: 'id', 'begin', 'end', 'rate')") + * @Rest\QueryParam(name="begin", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records after this date will be included (format: Y-m-d H:i:s)") + * @Rest\QueryParam(name="end", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records before this date will be included (format: Y-m-d H:i:s)") + * @Rest\QueryParam(name="exported", requirements="0|1", strict=true, nullable=true, description="Use this flag if you want to filter for export state (0=not exported, 1=exported, null=all") * * @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')") * @@ -130,6 +134,23 @@ class TimesheetController extends BaseApiController $query->setOrderBy($orderBy); } + if (null !== ($begin = $paramFetcher->get('begin'))) { + $query->setBegin(new \DateTime($begin)); + } + + if (null !== ($end = $paramFetcher->get('end'))) { + $query->setEnd(new \DateTime($end)); + } + + if (null !== ($exported = $paramFetcher->get('exported'))) { + $exported = (int) $exported; + if ($exported === 1) { + $query->setExported(TimesheetQuery::STATE_EXPORTED); + } elseif ($exported === 0) { + $query->setExported(TimesheetQuery::STATE_NOT_EXPORTED); + } + } + /** @var Pagerfanta $data */ $data = $this->repository->findByQuery($query); $data = (array) $data->getCurrentPageResults(); diff --git a/src/Configuration/LanguageFormattings.php b/src/Configuration/LanguageFormattings.php new file mode 100644 index 00000000..d2e43810 --- /dev/null +++ b/src/Configuration/LanguageFormattings.php @@ -0,0 +1,153 @@ +settings = $languageSettings; + } + + /** + * Returns an array with all available locale/language codes. + * + * @return string[] + */ + public function getAvailableLanguages(): array + { + return array_keys($this->settings); + } + + /** + * Returns the format which is used by the form component to handle date values. + * + * @param string $locale + * @return string + */ + public function getDateTypeFormat(string $locale): string + { + return $this->getConfig('date_type', $locale); + } + + /** + * Returns the format which is used by the Javascript component to handle date values. + * + * @param string $locale + * @return string + */ + public function getDatePickerFormat(string $locale): string + { + return $this->getConfig('date_picker', $locale); + } + + /** + * Returns the format which is used by the form component to handle datetime values. + * + * @param string $locale + * @return string + */ + public function getDateTimeTypeFormat(string $locale): string + { + return $this->getConfig('date_time_type', $locale); + } + + /** + * Returns the format which is used by the Javascript component to handle datetime values. + * + * @param string $locale + * @return string + */ + public function getDateTimePickerFormat(string $locale): string + { + return $this->getConfig('date_time_picker', $locale); + } + + /** + * Returns the locale specific date format, which should be used in combination with the twig filter "|date". + * + * @param string $locale + * @return string + */ + public function getDateFormat(string $locale): string + { + return $this->getConfig('date', $locale); + } + + /** + * Returns the locale specific time format, which should be used in combination with the twig filter "|time". + * + * @param string $locale + * @return string + */ + public function getTimeFormat(string $locale): string + { + return $this->getConfig('time', $locale); + } + + /** + * Returns the locale specific datetime format, which should be used in combination with the twig filter "|date". + * + * @param string $locale + * @return string + */ + public function getDateTimeFormat(string $locale): string + { + return $this->getConfig('date_time', $locale); + } + + /** + * Returns the format used in the "|duration" twig filter to display a Timesheet duration. + * + * @param string $locale + * @return string + */ + public function getDurationFormat(string $locale): string + { + return $this->getConfig('duration', $locale); + } + + /** + * Returns whether this locale uses the 24 hour format. + * + * @param string $locale + * @return bool + */ + public function isTwentyFourHours(string $locale): bool + { + return (bool) $this->getConfig('24_hours', $locale); + } + + /** + * @param string $key + * @param string $locale + * @return string + */ + protected function getConfig(string $key, string $locale): string + { + if (!isset($this->settings[$locale])) { + throw new \InvalidArgumentException(sprintf('Unknown locale given: %s', $locale)); + } + + if (!isset($this->settings[$locale][$key])) { + throw new \InvalidArgumentException(sprintf('Unknown setting for locale %s: %s', $locale, $key)); + } + + return $this->settings[$locale][$key]; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index df60bcb2..babe4539 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -38,7 +38,9 @@ class User extends BaseUser implements UserInterface public const ROLE_TEAMLEAD = 'ROLE_TEAMLEAD'; public const ROLE_ADMIN = 'ROLE_ADMIN'; public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN'; + public const DEFAULT_ROLE = self::ROLE_USER; + public const DEFAULT_LANGUAGE = 'en'; /** * @var int @@ -267,6 +269,14 @@ class User extends BaseUser implements UserInterface return null; } + /** + * @return string + */ + public function getLocale() + { + return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE); + } + /** * @param string $name * @param mixed $default diff --git a/src/Entity/UserPreference.php b/src/Entity/UserPreference.php index bffe1f31..b4551385 100644 --- a/src/Entity/UserPreference.php +++ b/src/Entity/UserPreference.php @@ -30,6 +30,8 @@ class UserPreference { public const HOURLY_RATE = 'hourly_rate'; public const SKIN = 'skin'; + public const LOCALE = 'language'; + public const TIMEZONE = 'timezone'; /** * @var int diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php index 49c2e441..0280c9d3 100644 --- a/src/EventSubscriber/UserPreferenceSubscriber.php +++ b/src/EventSubscriber/UserPreferenceSubscriber.php @@ -86,13 +86,13 @@ class UserPreferenceSubscriber implements EventSubscriberInterface ->addConstraint(new Range(['min' => 0])), (new UserPreference()) - ->setName('timezone') + ->setName(UserPreference::TIMEZONE) ->setValue(date_default_timezone_get()) ->setType(TimezoneType::class), (new UserPreference()) - ->setName('language') - ->setValue('en') + ->setName(UserPreference::LOCALE) + ->setValue(User::DEFAULT_LANGUAGE) ->setType(LanguageType::class), (new UserPreference()) diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 6ed1e37d..d945315c 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -268,11 +268,17 @@ class TimesheetEditForm extends AbstractType if ($options['include_rate']) { $builder ->add('fixedRate', MoneyType::class, [ + 'documentation' => [ + 'type' => 'float' + ], 'label' => 'label.fixedRate', 'required' => false, 'currency' => $currency, ]) ->add('hourlyRate', MoneyType::class, [ + 'documentation' => [ + 'type' => 'float' + ], 'label' => 'label.hourlyRate', 'required' => false, 'currency' => $currency, diff --git a/src/Form/Type/DateRangeType.php b/src/Form/Type/DateRangeType.php index 6b31821a..eb6c80b5 100644 --- a/src/Form/Type/DateRangeType.php +++ b/src/Form/Type/DateRangeType.php @@ -125,10 +125,19 @@ class DateRangeType extends AbstractType $pattern = $this->formatToPattern($formatDate, $separator); $builder->addModelTransformer(new CallbackTransformer( - function (DateRange $range) use ($formatDate, $separator) { + function ($range) use ($formatDate, $separator) { + if (null === $range) { + return ''; + } + + if (!($range instanceof DateRange)) { + throw new \InvalidArgumentException('Invalid DateRange given'); + } + if (null === $range->getBegin()) { return ''; } + $display = $range->getBegin()->format($formatDate); if (null !== $range->getEnd()) { $display .= $separator . $range->getEnd()->format($formatDate); diff --git a/src/Utils/LocaleSettings.php b/src/Utils/LocaleSettings.php index aebfa3a4..1fb2dec9 100644 --- a/src/Utils/LocaleSettings.php +++ b/src/Utils/LocaleSettings.php @@ -9,14 +9,18 @@ namespace App\Utils; +use App\Configuration\LanguageFormattings; use Symfony\Component\HttpFoundation\RequestStack; +/** + * Use this class, when you want information about formats for the "current request locale". + */ class LocaleSettings { /** * @var array */ - protected $settings; + protected $formats; /** * @var string @@ -25,15 +29,15 @@ class LocaleSettings /** * @param RequestStack $requestStack - * @param array $languageSettings + * @param LanguageFormattings $formats */ - public function __construct(RequestStack $requestStack, array $languageSettings) + public function __construct(RequestStack $requestStack, LanguageFormattings $formats) { // It can be null in a console command if (null !== $requestStack->getMasterRequest()) { $this->locale = $requestStack->getMasterRequest()->getLocale(); } - $this->settings = $languageSettings; + $this->formats = $formats; } /** @@ -43,7 +47,7 @@ class LocaleSettings */ public function getAvailableLanguages(): array { - return array_keys($this->settings); + return $this->formats->getAvailableLanguages(); } /** @@ -59,121 +63,90 @@ class LocaleSettings /** * Returns the format which is used by the form component to handle date values. * - * @param null|string $locale * @return string */ - public function getDateTypeFormat(?string $locale = null): string + public function getDateTypeFormat(): string { - return $this->getConfigByLocaleAndKey('date_type', $locale); + return $this->formats->getDateTypeFormat($this->getLocale()); } /** * Returns the format which is used by the Javascript component to handle date values. * - * @param null|string $locale * @return string */ - public function getDatePickerFormat(?string $locale = null): string + public function getDatePickerFormat(): string { - return $this->getConfigByLocaleAndKey('date_picker', $locale); + return $this->formats->getDatePickerFormat($this->getLocale()); } /** * Returns the format which is used by the form component to handle datetime values. * - * @param null|string $locale * @return string */ - public function getDateTimeTypeFormat(?string $locale = null): string + public function getDateTimeTypeFormat(): string { - return $this->getConfigByLocaleAndKey('date_time_type', $locale); + return $this->formats->getDateTimeTypeFormat($this->getLocale()); } /** * Returns the format which is used by the Javascript component to handle datetime values. * - * @param null|string $locale * @return string */ - public function getDateTimePickerFormat(?string $locale = null): string + public function getDateTimePickerFormat(): string { - return $this->getConfigByLocaleAndKey('date_time_picker', $locale); + return $this->formats->getDateTimePickerFormat($this->getLocale()); } /** * Returns the locale specific date format, which should be used in combination with the twig filter "|date". * - * @param null|string $locale * @return string */ - public function getDateFormat(?string $locale = null): string + public function getDateFormat(): string { - return $this->getConfigByLocaleAndKey('date', $locale); + return $this->formats->getDateFormat($this->getLocale()); } /** * Returns the locale specific time format, which should be used in combination with the twig filter "|time". * - * @param null|string $locale * @return string */ - public function getTimeFormat(?string $locale = null): string + public function getTimeFormat(): string { - return $this->getConfigByLocaleAndKey('time', $locale); + return $this->formats->getTimeFormat($this->getLocale()); } /** * Returns the locale specific datetime format, which should be used in combination with the twig filter "|date". * - * @param null|string $locale * @return string */ - public function getDateTimeFormat(?string $locale = null): string + public function getDateTimeFormat(): string { - return $this->getConfigByLocaleAndKey('date_time', $locale); + return $this->formats->getDateTimeFormat($this->getLocale()); } /** * Returns the format used in the "|duration" twig filter to display a Timesheet duration. * - * @param null|string $locale * @return string */ - public function getDurationFormat(?string $locale = null): string + public function getDurationFormat(): string { - return $this->getConfigByLocaleAndKey('duration', $locale); + return $this->formats->getDurationFormat($this->getLocale()); } /** * Returns whether this locale uses the 24 hour format. * - * @param null|string $locale * @return bool */ - public function isTwentyFourHours(?string $locale = null): bool + public function isTwentyFourHours(): bool { - return (bool) $this->getConfigByLocaleAndKey('24_hours', $locale); - } - - /** - * @param string $key - * @param null|string $locale - * @return string - */ - protected function getConfigByLocaleAndKey(string $key, ?string $locale = null): string - { - if (null === $locale) { - $locale = $this->getLocale(); - } - - if (!isset($this->settings[$locale])) { - throw new \InvalidArgumentException(sprintf('Unknown locale given: %s', $locale)); - } - - if (!isset($this->settings[$locale][$key])) { - throw new \InvalidArgumentException(sprintf('Unknown setting for locale %s: %s', $locale, $key)); - } - - return $this->settings[$locale][$key]; + return $this->formats->isTwentyFourHours($this->getLocale()); } } diff --git a/tests/API/ConfigurationControllerTest.php b/tests/API/ConfigurationControllerTest.php new file mode 100644 index 00000000..c6151f4d --- /dev/null +++ b/tests/API/ConfigurationControllerTest.php @@ -0,0 +1,52 @@ +assertUrlIsSecured('/api/config/i18n'); + } + + public function testGetI18n() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/config/i18n', 'GET'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + $this->assertEquals(7, count($result)); + $this->assertStructure($result, false); + } + + protected function assertStructure(array $result, $full = true) + { + $expectedKeys = ['date', 'date_time', 'duration', 'form_date', 'form_date_time', 'is24hours', 'time']; + $actual = array_keys($result); + sort($actual); + sort($expectedKeys); + + $this->assertEquals($expectedKeys, $actual, 'Activity structure does not match'); + } +} diff --git a/tests/API/Model/I18nTest.php b/tests/API/Model/I18nTest.php new file mode 100644 index 00000000..4cafa433 --- /dev/null +++ b/tests/API/Model/I18nTest.php @@ -0,0 +1,52 @@ +assertTrue($sut->isIs24hours()); + $this->assertEquals('', $sut->getDuration()); + $this->assertEquals('', $sut->getDate()); + $this->assertEquals('', $sut->getDateTime()); + $this->assertEquals('', $sut->getFormDate()); + $this->assertEquals('', $sut->getFormDateTime()); + $this->assertEquals('', $sut->getTime()); + } + + public function testSetter() + { + $sut = new I18n(); + + $this->assertInstanceOf(I18n::class, $sut->setIs24hours(false)); + $this->assertInstanceOf(I18n::class, $sut->setDuration('foo')); + $this->assertInstanceOf(I18n::class, $sut->setDate('bar')); + $this->assertInstanceOf(I18n::class, $sut->setDateTime('hello')); + $this->assertInstanceOf(I18n::class, $sut->setFormDate('world')); + $this->assertInstanceOf(I18n::class, $sut->setFormDateTime('testing')); + $this->assertInstanceOf(I18n::class, $sut->setTime('fun')); + + $this->assertFalse($sut->isIs24hours()); + $this->assertEquals('foo', $sut->getDuration()); + $this->assertEquals('bar', $sut->getDate()); + $this->assertEquals('hello', $sut->getDateTime()); + $this->assertEquals('world', $sut->getFormDate()); + $this->assertEquals('testing', $sut->getFormDateTime()); + $this->assertEquals('fun', $sut->getTime()); + } +} diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index fcd3ecef..c7df2101 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -119,7 +119,23 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testGetCollectionWithQuery() { - $query = ['customer' => 1, 'project' => 1, 'activity' => 1, 'page' => 2, 'size' => 5, 'order' => 'DESC', 'orderBy' => 'rate']; + $begin = new \DateTime('-10 days'); + $begin->setTime(0, 0, 0); + $end = new \DateTime(); + $end->setTime(23, 59, 59); + + $query = [ + 'customer' => 1, + 'project' => 1, + 'activity' => 1, + 'page' => 2, + 'size' => 5, + 'order' => 'DESC', + 'orderBy' => 'rate', + 'begin' => $begin->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + 'exported' => 0, + ]; $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); $result = json_decode($client->getResponse()->getContent(), true); @@ -130,6 +146,74 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertDefaultStructure($result[0], false); } + public function testExportedFilter() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + + $fixture = new TimesheetFixtures(); + $fixture + ->setExported(true) + ->setAmount(7) + ->setUser($this->getUserByRole($em, User::ROLE_USER)) + ->setStartDate(new \DateTime('-10 days')) + ->setAllowEmptyDescriptions(false) + ; + $this->importFixture($em, $fixture); + + $begin = new \DateTime('-10 days'); + $begin->setTime(0, 0, 0); + $end = new \DateTime(); + $end->setTime(23, 59, 59); + + $query = [ + 'page' => 1, + 'size' => 50, + 'begin' => $begin->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + 'exported' => 1, + ]; + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + $this->assertEquals(7, count($result)); + $this->assertDefaultStructure($result[0], false); + + $query = [ + 'page' => 1, + 'size' => 50, + 'begin' => $begin->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + 'exported' => 0, + ]; + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + $this->assertEquals(10, count($result)); + $this->assertDefaultStructure($result[0], false); + + $query = [ + 'page' => 1, + 'size' => 50, + 'begin' => $begin->format('Y-m-d H:i:s'), + 'end' => $end->format('Y-m-d H:i:s'), + ]; + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + $this->assertEquals(17, count($result)); + $this->assertDefaultStructure($result[0], false); + } + public function testGetEntity() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); diff --git a/tests/Configuration/LanguageFormattingsTest.php b/tests/Configuration/LanguageFormattingsTest.php new file mode 100644 index 00000000..a8c2de99 --- /dev/null +++ b/tests/Configuration/LanguageFormattingsTest.php @@ -0,0 +1,195 @@ + [ + 'date_time_type' => 'dd.MM.yyyy HH:mm', + 'date_time_picker' => 'DD.MM.YYYY HH:mm', + 'date_type' => 'dd.MM.yyyy', + 'date_picker' => 'DD.MM.YYYY', + 'date' => 'd.m.Y', + 'date_time' => 'd.m. H:i', + 'duration' => '%h:%m h', + 'time' => 'H:i', + '24_hours' => true, + ], + 'en' => [ + 'date_time_type' => 'yyyy-MM-dd HH:mm', + 'date_time_picker' => 'YYYY-MM-DD HH:mm', + 'date_type' => 'yyyy-MM-dd', + 'date_picker' => 'YYYY-MM-DD', + 'date' => 'Y-m-d', + 'date_time' => 'm-d H:i', + 'duration' => '%h:%m h', + 'time' => 'H:i:s', + '24_hours' => false, + ], + 'pt_BR' => [ + 'date_time_type' => 'dd-MM-yyyy HH:mm', + 'date_time_picker' => 'DD-MM-YYYY HH:mm', + 'date_type' => 'dd-MM-yyyy', + 'date_picker' => 'DD-MM-YYYY', + 'date' => 'd-m-Y', + 'duration' => '%h:%m h', + ], + 'it' => [ + 'date_time_type' => 'dd.MM.yyyy HH:mm', + 'date_time_picker' => 'DD.MM.YYYY HH:mm', + 'date_type' => 'dd.MM.yyyy', + 'date_picker' => 'DD.MM.YYYY', + 'date' => 'd.m.Y', + 'duration' => '%h:%m h', + ], + 'fr' => [ + 'date_time_type' => 'dd/MM/yyyy HH:mm', + 'date_time_picker' => 'DD/MM/YYYY HH:mm', + 'date_type' => 'dd/MM/yyyy', + 'date_picker' => 'DD/MM/YYYY', + 'date' => 'd/m/Y', + 'duration' => '%h h %m', + ], + 'es' => [ + 'date_time_type' => 'dd.MM.yyyy HH:mm', + 'date_time_picker' => 'DD.MM.YYYY HH:mm', + 'date_type' => 'dd.MM.yyyy', + 'date_picker' => 'DD.MM.YYYY', + 'date' => 'd.m.Y', + 'duration' => '%h:%m h', + ], + 'ru' => [ + 'date_time_type' => 'dd.MM.yyyy HH:mm', + 'date_time_picker' => 'DD.MM.YYYY HH:mm', + 'date_type' => 'dd.MM.yyyy', + 'date_picker' => 'DD.MM.YYYY', + 'date' => 'd.m.Y', + 'duration' => '%h:%m h', + ], + 'ar' => [ + 'date_time_type' => 'yyyy-MM-dd HH:mm', + 'date_time_picker' => 'YYYY-MM-DD HH:mm', + 'date_type' => 'yyyy-MM-dd', + 'date_picker' => 'YYYY-MM-DD', + 'date' => 'Y-m-d', + 'duration' => '%h:%m h', + ], + 'hu' => [ + 'date_time_type' => 'yyyy.MM.dd HH:mm', + 'date_time_picker' => 'YYYY.MM.DD HH:mm', + 'date_type' => 'yyyy.MM.dd', + 'date_picker' => 'YYYY.MM.DD', + 'date' => 'Y.m.d.', + 'duration' => '%h:%m h', + ], + ]; + } + + public function testGetAvailableLanguages() + { + $sut = $this->getSut([]); + $this->assertEquals([], $sut->getAvailableLanguages()); + + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals(['de', 'en', 'pt_BR', 'it', 'fr', 'es', 'ru', 'ar', 'hu'], $sut->getAvailableLanguages()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unknown locale given: xx + */ + public function testInvalidLocaleWithGivenLocale() + { + $sut = $this->getSut($this->getDefaultSettings()); + $sut->getDateFormat('xx'); + } + + public function testGetDurationFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('%h:%m h', $sut->getDurationFormat('de')); + } + + public function testGetDateFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('d.m.Y', $sut->getDateFormat('de')); + } + + public function testGetDateTimeFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('d.m. H:i', $sut->getDateTimeFormat('de')); + } + + public function testGetDateTypeFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('dd.MM.yyyy', $sut->getDateTypeFormat('de')); + } + + public function testGetDatePickerFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('DD.MM.YYYY', $sut->getDatePickerFormat('de')); + } + + public function testGetDateTimeTypeFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('dd.MM.yyyy HH:mm', $sut->getDateTimeTypeFormat('de')); + } + + public function testGetDateTimePickerFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('DD.MM.YYYY HH:mm', $sut->getDateTimePickerFormat('de')); + } + + public function testIs24Hours() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertTrue($sut->isTwentyFourHours('de')); + $this->assertFalse($sut->isTwentyFourHours('en')); + } + + public function testGetTimeFormat() + { + $sut = $this->getSut($this->getDefaultSettings()); + $this->assertEquals('H:i', $sut->getTimeFormat('de')); + $this->assertEquals('H:i:s', $sut->getTimeFormat('en')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unknown setting for locale en: date_time_picker + */ + public function testUnknownSetting() + { + $sut = $this->getSut(['en' => [ + 'xxx' => 'dd.MM.yyyy HH:mm', + ]]); + $sut->getDateTimePickerFormat('en'); + } +} diff --git a/tests/DataFixtures/TimesheetFixtures.php b/tests/DataFixtures/TimesheetFixtures.php index cd681346..77c0d559 100644 --- a/tests/DataFixtures/TimesheetFixtures.php +++ b/tests/DataFixtures/TimesheetFixtures.php @@ -56,6 +56,10 @@ class TimesheetFixtures extends Fixture * @var bool */ protected $allowEmptyDescriptions = true; + /** + * @var int + */ + protected $exported = false; /** * @param bool $allowEmptyDescriptions @@ -68,6 +72,17 @@ class TimesheetFixtures extends Fixture return $this; } + /** + * @param bool $exported + * @return TimesheetFixtures + */ + public function setExported(bool $exported) + { + $this->exported = $exported; + + return $this; + } + /** * @param bool $fixedRate * @return TimesheetFixtures @@ -292,6 +307,10 @@ class TimesheetFixtures extends Fixture $entry->setHourlyRate($hourlyRate); } + if (null !== $this->exported) { + $entry->setExported($this->exported); + } + if ($setEndDate) { $entry ->setEnd($end) diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index 9333c458..eb40b465 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -107,4 +107,17 @@ class UserTest extends AbstractEntityTest $this->assertEquals('foo', (string) $user); $this->assertEquals('foo', $user->getAlias()); } + + public function testGetLocale() + { + $sut = new User(); + $this->assertEquals(User::DEFAULT_LANGUAGE, $sut->getLocale()); + + $language = new UserPreference(); + $language->setName(UserPreference::LOCALE); + $language->setValue('fr'); + $sut->addPreference($language); + + $this->assertEquals('fr', $sut->getLocale()); + } } diff --git a/tests/Export/Renderer/AbstractRendererTest.php b/tests/Export/Renderer/AbstractRendererTest.php index 34367d88..5da4bbf3 100644 --- a/tests/Export/Renderer/AbstractRendererTest.php +++ b/tests/Export/Renderer/AbstractRendererTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Export\Renderer; +use App\Configuration\LanguageFormattings; use App\Entity\Activity; use App\Entity\Customer; use App\Entity\Project; @@ -45,7 +46,7 @@ abstract class AbstractRendererTest extends KernelTestCase $request->setLocale('en'); $requestStack->push($request); - $localeSettings = new LocaleSettings($requestStack, $languages); + $localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages)); $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $dateExtension = new DateExtensions($localeSettings); diff --git a/tests/Invoice/Renderer/AbstractRendererTest.php b/tests/Invoice/Renderer/AbstractRendererTest.php index 48e57818..9c5cec81 100644 --- a/tests/Invoice/Renderer/AbstractRendererTest.php +++ b/tests/Invoice/Renderer/AbstractRendererTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Invoice\Renderer; +use App\Configuration\LanguageFormattings; use App\Entity\Activity; use App\Entity\Customer; use App\Entity\InvoiceDocument; @@ -69,7 +70,7 @@ abstract class AbstractRendererTest extends KernelTestCase $request->setLocale('en'); $requestStack->push($request); - $localeSettings = new LocaleSettings($requestStack, $languages); + $localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages)); $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $dateExtension = new DateExtensions($localeSettings); diff --git a/tests/Twig/DateExtensionsTest.php b/tests/Twig/DateExtensionsTest.php index e87217a2..a4aa26f1 100644 --- a/tests/Twig/DateExtensionsTest.php +++ b/tests/Twig/DateExtensionsTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Twig; +use App\Configuration\LanguageFormattings; use App\Twig\DateExtensions; use App\Utils\LocaleSettings; use PHPUnit\Framework\TestCase; @@ -33,7 +34,7 @@ class DateExtensionsTest extends TestCase $requestStack = new RequestStack(); $requestStack->push($request); - $localeSettings = new LocaleSettings($requestStack, $dateSettings); + $localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($dateSettings)); return new DateExtensions($localeSettings); } diff --git a/tests/Twig/ExtensionsTest.php b/tests/Twig/ExtensionsTest.php index 63aa394a..6690b2e7 100644 --- a/tests/Twig/ExtensionsTest.php +++ b/tests/Twig/ExtensionsTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Twig; +use App\Configuration\LanguageFormattings; use App\Entity\Timesheet; use App\Entity\User; use App\Twig\Extensions; @@ -41,7 +42,7 @@ class ExtensionsTest extends TestCase $requestStack = new RequestStack(); $requestStack->push($request); - $localeSettings = new LocaleSettings($requestStack, $locales); + $localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($locales)); return new Extensions($requestStack, $localeSettings); } diff --git a/tests/Utils/LocaleSettingsTest.php b/tests/Utils/LocaleSettingsTest.php index fb2bc73c..1521e078 100644 --- a/tests/Utils/LocaleSettingsTest.php +++ b/tests/Utils/LocaleSettingsTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Utils; +use App\Configuration\LanguageFormattings; use App\Utils\LocaleSettings; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -16,6 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack; /** * @covers \App\Utils\LocaleSettings + * @covers \App\Configuration\LanguageFormattings */ class LocaleSettingsTest extends TestCase { @@ -31,7 +33,7 @@ class LocaleSettingsTest extends TestCase protected function getSut(string $locale, array $settings) { - return new LocaleSettings($this->getRequestStack($locale), $settings); + return new LocaleSettings($this->getRequestStack($locale), new LanguageFormattings($settings)); } protected function getDefaultSettings() @@ -45,6 +47,8 @@ class LocaleSettingsTest extends TestCase 'date' => 'd.m.Y', 'date_time' => 'd.m. H:i', 'duration' => '%h:%m h', + 'time' => 'H:i', + '24_hours' => true, ], 'en' => [ 'date_time_type' => 'yyyy-MM-dd HH:mm', @@ -54,6 +58,8 @@ class LocaleSettingsTest extends TestCase 'date' => 'Y-m-d', 'date_time' => 'm-d H:i', 'duration' => '%h:%m h', + 'time' => 'H:i:s', + '24_hours' => false, ], 'pt_BR' => [ 'date_time_type' => 'dd-MM-yyyy HH:mm', @@ -146,57 +152,62 @@ class LocaleSettingsTest extends TestCase */ public function testInvalidLocaleWithGivenLocale() { - $sut = $this->getSut('en', $this->getDefaultSettings()); - $sut->getDateFormat('xx'); + $sut = $this->getSut('xx', $this->getDefaultSettings()); + $sut->getDateFormat(); } public function testGetDurationFormat() { $sut = $this->getSut('en', $this->getDefaultSettings()); $this->assertEquals('%h:%m h', $sut->getDurationFormat()); - $this->assertEquals('%h:%m h', $sut->getDurationFormat('de')); } public function testGetDateFormat() { - $sut = $this->getSut('en', $this->getDefaultSettings()); - $this->assertEquals('Y-m-d', $sut->getDateFormat()); - $this->assertEquals('d.m.Y', $sut->getDateFormat('de')); + $sut = $this->getSut('de', $this->getDefaultSettings()); + $this->assertEquals('d.m.Y', $sut->getDateFormat()); } public function testGetDateTimeFormat() { $sut = $this->getSut('en', $this->getDefaultSettings()); $this->assertEquals('m-d H:i', $sut->getDateTimeFormat()); - $this->assertEquals('d.m. H:i', $sut->getDateTimeFormat('de')); } public function testGetDateTypeFormat() { - $sut = $this->getSut('en', $this->getDefaultSettings()); - $this->assertEquals('yyyy-MM-dd', $sut->getDateTypeFormat()); - $this->assertEquals('dd.MM.yyyy', $sut->getDateTypeFormat('de')); + $sut = $this->getSut('de', $this->getDefaultSettings()); + $this->assertEquals('dd.MM.yyyy', $sut->getDateTypeFormat()); } public function testGetDatePickerFormat() { $sut = $this->getSut('en', $this->getDefaultSettings()); $this->assertEquals('YYYY-MM-DD', $sut->getDatePickerFormat()); - $this->assertEquals('DD.MM.YYYY', $sut->getDatePickerFormat('de')); } public function testGetDateTimeTypeFormat() { $sut = $this->getSut('en', $this->getDefaultSettings()); $this->assertEquals('yyyy-MM-dd HH:mm', $sut->getDateTimeTypeFormat()); - $this->assertEquals('dd.MM.yyyy HH:mm', $sut->getDateTimeTypeFormat('de')); } public function testGetDateTimePickerFormat() { $sut = $this->getSut('en', $this->getDefaultSettings()); $this->assertEquals('YYYY-MM-DD HH:mm', $sut->getDateTimePickerFormat()); - $this->assertEquals('DD.MM.YYYY HH:mm', $sut->getDateTimePickerFormat('de')); + } + + public function testIs24Hours() + { + $sut = $this->getSut('en', $this->getDefaultSettings()); + $this->assertFalse($sut->isTwentyFourHours()); + } + + public function testGetTimeFormat() + { + $sut = $this->getSut('en', $this->getDefaultSettings()); + $this->assertEquals('H:i:s', $sut->getTimeFormat()); } /** @@ -208,6 +219,6 @@ class LocaleSettingsTest extends TestCase $sut = $this->getSut('en', ['en' => [ 'xxx' => 'dd.MM.yyyy HH:mm', ]]); - $sut->getDateTimePickerFormat('en'); + $sut->getDateTimePickerFormat(); } }