fix timezone problems in timesheet forms (#555)

This commit is contained in:
Kevin Papst
2019-02-13 21:37:54 +01:00
committed by GitHub
parent 2c6f57c7ce
commit ef33233624
23 changed files with 461 additions and 61 deletions

View File

@@ -15,6 +15,7 @@ use App\Entity\Timesheet;
use App\Form\TimesheetEditForm;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Request\ParamFetcherInterface;
@@ -46,16 +47,23 @@ class TimesheetController extends BaseApiController
*/
protected $hardLimit;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param ViewHandlerInterface $viewHandler
* @param TimesheetRepository $repository
* @param UserDateTimeFactory $dateTime
* @param int $hardLimit
*/
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit)
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, int $hardLimit)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->hardLimit = $hardLimit;
$this->dateTime = $dateTime;
}
/**
@@ -164,7 +172,7 @@ class TimesheetController extends BaseApiController
{
$timesheet = new Timesheet();
$timesheet->setUser($this->getUser());
$timesheet->setBegin(new \DateTime());
$timesheet->setBegin($this->dateTime->createDateTime());
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
'csrf_protection' => false,

View File

@@ -15,6 +15,7 @@ use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Entity\UserPreference;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
@@ -125,7 +126,7 @@ class KimaiImporterCommand extends Command
)
->addArgument('prefix', InputArgument::REQUIRED, 'The database prefix for the old Kimai v1 tables')
->addArgument('password', InputArgument::REQUIRED, 'The new password for all imported user')
->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer', 'de')
->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer (2-character uppercase)', 'DE')
;
}
@@ -249,7 +250,7 @@ class KimaiImporterCommand extends Command
$allImports = 0;
try {
$counter = $this->importUsers($io, $password, $users);
$counter = $this->importUsers($io, $password, $users, $rates);
$allImports += $counter;
$io->success('Imported users: ' . $counter);
} catch (\Exception $ex) {
@@ -398,16 +399,21 @@ class KimaiImporterCommand extends Command
}
/**
* @param string $table
* @param $table
* @param array $where
* @return array
*/
protected function fetchAllFromImport($table)
protected function fetchAllFromImport($table, array $where = [])
{
return $this->connection->createQueryBuilder()
$query = $this->connection->createQueryBuilder()
->select('*')
->from($this->connection->quoteIdentifier($this->dbPrefix . $table))
->execute()
->fetchAll();
->from($this->connection->quoteIdentifier($this->dbPrefix . $table));
foreach ($where as $column => $value) {
$query->andWhere($query->expr()->eq($column, $value));
}
return $query->execute()->fetchAll();
}
/**
@@ -467,10 +473,11 @@ class KimaiImporterCommand extends Command
* @param SymfonyStyle $io
* @param string $password
* @param array $users
* @param array $rates
* @return int
* @throws \Exception
*/
protected function importUsers(SymfonyStyle $io, $password, $users)
protected function importUsers(SymfonyStyle $io, $password, $users, $rates)
{
$counter = 0;
$entityManager = $this->getDoctrine()->getManager();
@@ -495,6 +502,34 @@ class KimaiImporterCommand extends Command
throw new \Exception('Failed to validate user: ' . $user->getUsername());
}
// find and migrate user preferences
$prefsToImport = ['ui.lang' => 'language', 'timezone' => 'timezone'];
$preferences = $this->fetchAllFromImport('preferences', ['userID' => $oldUser['userID']]);
foreach ($preferences as $pref) {
$key = $pref['option'];
if (!array_key_exists($key, $prefsToImport)) {
continue;
}
$newPref = new UserPreference();
$newPref
->setName($prefsToImport[$key])
->setValue($pref['value']);
$user->addPreference($newPref);
}
// find hourly rate
foreach ($rates as $ratesRow) {
if ($ratesRow['userID'] === $oldUser['userID'] && $ratesRow['activityID'] === null && $ratesRow['projectID'] === null) {
$newPref = new UserPreference();
$newPref
->setName(UserPreference::HOURLY_RATE)
->setValue($ratesRow['rate']);
$user->addPreference($newPref);
}
}
try {
$entityManager->persist($user);
$entityManager->flush();
@@ -571,7 +606,7 @@ class KimaiImporterCommand extends Command
->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city'])
->setTimezone($oldCustomer['timezone'])
->setVisible($isActive)
->setCountry($country)
->setCountry(strtoupper($country))
;
if (!$this->validateImport($io, $customer)) {
@@ -908,12 +943,18 @@ class KimaiImporterCommand extends Command
}
if (null === $activity) {
$io->error('Could not create timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId);
$io->error('Could not import timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId);
continue;
}
$duration = $oldRecord['end'] - $oldRecord['start'];
// FIXME create user on the fly
if (!isset($this->users[$oldRecord['userID']])) {
$io->error('Could not import timesheet record, unknown user: ' . $oldRecord['userID']);
continue;
}
$timesheet = new Timesheet();
$fixedRate = $oldRecord['fixedRate'];
@@ -934,15 +975,30 @@ class KimaiImporterCommand extends Command
$timesheet->setRate(round($rate, 2));
}
$user = $this->users[$oldRecord['userID']];
$timezone = $user->getPreferenceValue('timezone', date_default_timezone_get());
$dateTimezone = new \DateTimeZone('UTC');
$begin = new \DateTime('@' . $oldRecord['start']);
$begin->setTimezone($dateTimezone);
$end = new \DateTime('@' . $oldRecord['end']);
$end->setTimezone($dateTimezone);
// ---------- workaround for localizeDates ----------
// if getBegin() is not execute first, then the dates will we re-written in validateImport() below
$timesheet->setBegin($begin)->setEnd($end)->getBegin();
// --------------------------------------------------
$timesheet
->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null))
->setUser($this->users[$oldRecord['userID']])
->setBegin(new \DateTime('@' . $oldRecord['start']))
->setEnd(new \DateTime('@' . $oldRecord['end']))
->setBegin($begin)
->setEnd($end)
->setDuration($duration)
->setActivity($activity)
->setProject($project)
->setExported(intval($oldRecord['cleared']) !== 0)
->setTimezone($timezone)
;
if (!$this->validateImport($io, $timesheet)) {

View File

@@ -13,6 +13,7 @@ use App\Calendar\Service;
use App\Calendar\TimesheetEntity;
use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
@@ -30,13 +31,19 @@ class CalendarController extends AbstractController
* @var Service
*/
protected $calendar;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param Service $calendar
* @param UserDateTimeFactory $dateTime
*/
public function __construct(Service $calendar)
public function __construct(Service $calendar, UserDateTimeFactory $dateTime)
{
$this->calendar = $calendar;
$this->dateTime = $dateTime;
}
/**
@@ -47,7 +54,8 @@ class CalendarController extends AbstractController
{
return $this->render('calendar/user.html.twig', [
'config' => $this->calendar->getConfig(),
'google' => $this->calendar->getGoogle()
'google' => $this->calendar->getGoogle(),
'now' => $this->dateTime->createDateTime(),
]);
}

View File

@@ -18,6 +18,7 @@ use App\Form\UserPasswordType;
use App\Form\UserPreferencesForm;
use App\Form\UserRolesType;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory;
use App\Voter\UserVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -45,12 +46,20 @@ class ProfileController extends AbstractController
protected $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
* @var UserDateTimeFactory
*/
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
protected $dateTime;
/**
* @param UserPasswordEncoderInterface $encoder
* @param EventDispatcherInterface $dispatcher
* @param UserDateTimeFactory $dateTime
*/
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime)
{
$this->encoder = $encoder;
$this->dispatcher = $dispatcher;
$this->dateTime = $dateTime;
}
/**
@@ -252,6 +261,7 @@ class ProfileController extends AbstractController
'user' => $user,
'stats' => $userStats,
'years' => $monthlyStats,
'local_time' => $this->dateTime->createDateTime(),
'forms' => []
];

View File

@@ -29,14 +29,6 @@ class TimesheetController extends AbstractController
{
use TimesheetControllerTrait;
/**
* @param int $hardLimit
*/
public function __construct(int $hardLimit)
{
$this->setHardLimit($hardLimit);
}
/**
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
@@ -162,7 +154,7 @@ class TimesheetController extends AbstractController
try {
$entry = new Timesheet();
$entry
->setBegin(new \DateTime())
->setBegin($this->dateTime->createDateTime())
->setUser($user)
->setActivity($timesheet->getActivity())
->setProject($timesheet->getProject())

View File

@@ -12,6 +12,7 @@ namespace App\Controller;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -27,6 +28,21 @@ trait TimesheetControllerTrait
*/
private $hardLimit = 1;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param UserDateTimeFactory $dateTime
* @param int $hardLimit
*/
public function __construct(UserDateTimeFactory $dateTime, int $hardLimit)
{
$this->dateTime = $dateTime;
$this->setHardLimit($hardLimit);
}
/**
* @param int $hardLimit
*/
@@ -118,7 +134,7 @@ trait TimesheetControllerTrait
{
$entry = new Timesheet();
$entry->setUser($this->getUser());
$entry->setBegin(new \DateTime());
$entry->setBegin($this->dateTime->createDateTime());
$start = $request->get('begin');
if ($start !== null) {

View File

@@ -16,9 +16,6 @@ use KevinPapst\AdminLTEBundle\Model\UserModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Class NavbarShowUserSubscriber
*/
class NavbarShowUserSubscriber implements EventSubscriberInterface
{
/**
@@ -27,7 +24,6 @@ class NavbarShowUserSubscriber implements EventSubscriberInterface
protected $storage;
/**
* NavbarShowUserListener constructor.
* @param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
@@ -64,7 +60,7 @@ class NavbarShowUserSubscriber implements EventSubscriberInterface
->setIsOnline(true)
->setTitle($myUser->getTitle())
->setAvatar($myUser->getAvatar())
->setMemberSince(new \DateTime());
->setMemberSince($myUser->getRegisteredAt());
$event->setUser($user);
}

View File

@@ -46,7 +46,7 @@ class TimezoneSubscriber implements EventSubscriberInterface
*/
public function setTimezone(GetResponseEvent $event)
{
if (null === $this->storage->getToken()) {
if (!$this->canHandleEvent()) {
return;
}
@@ -55,4 +55,24 @@ class TimezoneSubscriber implements EventSubscriberInterface
$timezone = $user->getPreferenceValue('timezone', date_default_timezone_get());
date_default_timezone_set($timezone);
}
/**
* @return bool
*/
protected function canHandleEvent(): bool
{
if (null === $this->storage->getToken()) {
return false;
}
/* @var $user User */
$user = $this->storage->getToken()->getUser();
if (null === $user) {
return false;
}
return ($user instanceof User);
}
}

View File

@@ -12,6 +12,7 @@ namespace App\Export\Renderer;
use App\Entity\Timesheet;
use App\Export\RendererInterface;
use App\Repository\Query\TimesheetQuery;
use App\Timesheet\UserDateTimeFactory;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
use Symfony\Component\HttpFoundation\Response;
@@ -27,11 +28,18 @@ class PDFRenderer implements RendererInterface
protected $twig;
/**
* @param \Twig_Environment $twig
* @var UserDateTimeFactory
*/
public function __construct(\Twig_Environment $twig)
protected $dateTime;
/**
* @param \Twig_Environment $twig
* @param UserDateTimeFactory $dateTime
*/
public function __construct(\Twig_Environment $twig, UserDateTimeFactory $dateTime)
{
$this->twig = $twig;
$this->dateTime = $dateTime;
}
/**
@@ -48,7 +56,7 @@ class PDFRenderer implements RendererInterface
$content = $this->twig->render('export/renderer/pdf.html.twig', [
'entries' => $timesheets,
'query' => $query,
'now' => new \DateTime(),
'now' => $this->dateTime->createDateTime(),
'summaries' => $this->calculateSummary($timesheets),
]);

View File

@@ -20,6 +20,7 @@ use App\Form\Type\YesNoType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Timesheet\UserDateTimeFactory;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@@ -48,15 +49,22 @@ class TimesheetEditForm extends AbstractType
*/
private $durationOnly = false;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param CustomerRepository $customer
* @param ProjectRepository $project
* @param UserDateTimeFactory $dateTime
* @param bool $durationOnly
*/
public function __construct(CustomerRepository $customer, ProjectRepository $project, bool $durationOnly)
public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime, bool $durationOnly)
{
$this->customers = $customer;
$this->projects = $project;
$this->dateTime = $dateTime;
$this->durationOnly = $durationOnly;
}
@@ -70,6 +78,7 @@ class TimesheetEditForm extends AbstractType
$customer = null;
$currency = false;
$end = null;
$begin = null;
if (isset($options['data'])) {
/** @var Timesheet $entry */
@@ -87,12 +96,21 @@ class TimesheetEditForm extends AbstractType
$currency = $customer->getCurrency();
}
$begin = $entry->getBegin();
$end = $entry->getEnd();
}
$timezone = $this->dateTime->getTimezone()->getName();
if (null !== $begin) {
$timezone = $begin->getTimezone()->getName();
}
if (null === $end || !$options['duration_only']) {
$builder->add('begin', DateTimePickerType::class, [
'label' => 'label.begin',
'model_timezone' => $timezone,
'view_timezone' => $timezone,
]);
}
@@ -102,7 +120,9 @@ class TimesheetEditForm extends AbstractType
]);
} else {
$builder->add('end', DateTimePickerType::class, [
'label' => 'label.end',
'label' => 'label.begin',
'model_timezone' => $timezone,
'view_timezone' => $timezone,
'required' => false,
]);
}
@@ -135,7 +155,10 @@ class TimesheetEditForm extends AbstractType
}
$builder
->add('project', ProjectType::class, array_merge($projectOptions, [
->add(
'project',
ProjectType::class,
array_merge($projectOptions, [
'placeholder' => '',
'activity_enabled' => true,
// documentation is for NelmioApiDocBundle

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
@@ -26,11 +27,18 @@ class DatePickerType extends AbstractType
protected $localeSettings;
/**
* @param LocaleSettings $localeSettings
* @var UserDateTimeFactory
*/
public function __construct(LocaleSettings $localeSettings)
protected $dateTime;
/**
* @param LocaleSettings $localeSettings
* @param UserDateTimeFactory $dateTime
*/
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
{
$this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
}
/**
@@ -40,12 +48,15 @@ class DatePickerType extends AbstractType
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([
'widget' => 'single_text',
'html5' => false,
'format' => $dateFormat,
'format_picker' => $pickerFormat,
'model_timezone' => $timezone,
'view_timezone' => $timezone,
]);
$resolver->setDefault('attr', function (Options $options) {

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
@@ -25,12 +26,18 @@ class DateTimePickerType extends AbstractType
*/
protected $localeSettings;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param LocaleSettings $localeSettings
*/
public function __construct(LocaleSettings $localeSettings)
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
{
$this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
}
/**
@@ -40,6 +47,7 @@ class DateTimePickerType extends AbstractType
{
$dateTimePicker = $this->localeSettings->getDateTimePickerFormat();
$dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([
'label' => 'label.begin',
@@ -48,6 +56,8 @@ class DateTimePickerType extends AbstractType
'format' => $dateTimeFormat,
'format_picker' => $dateTimePicker,
'with_seconds' => false,
'model_timezone' => $timezone,
'view_timezone' => $timezone,
]);
$resolver->setDefault('attr', function (Options $options) {

View File

@@ -55,8 +55,11 @@ class TimesheetRepository extends AbstractRepository
}
// seems to be necessary so Doctrine will recognize a changed timestamp
$entry->setBegin(clone $entry->getBegin());
$entry->setEnd(new DateTime());
$begin = clone $entry->getBegin();
$end = new \DateTime('now', $begin->getTimezone());
$entry->setBegin($begin);
$entry->setEnd($end);
$entityManager = $this->getEntityManager();
$entityManager->persist($entry);
@@ -103,11 +106,11 @@ class TimesheetRepository extends AbstractRepository
/**
* @param $select
* @param User|null $user
* @param User $user
* @return int
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function queryThisMonth($select, ?User $user)
protected function queryThisMonth($select, User $user)
{
$begin = new DateTime('first day of this month 00:00:00');
$end = new DateTime('last day of this month 23:59:59');

View File

@@ -0,0 +1,82 @@
<?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\Timesheet;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class UserDateTimeFactory
{
/**
* @var \DateTimeZone
*/
protected $timezone;
/**
* @param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
if (null === $tokenStorage->getToken()) {
return;
}
/* @var $user User */
$user = $tokenStorage->getToken()->getUser();
$timezone = date_default_timezone_get();
if ($user instanceof User && null !== $user->getPreferenceValue('timezone')) {
$timezone = $user->getPreferenceValue('timezone');
}
$this->timezone = new \DateTimeZone($timezone);
}
/**
* @return \DateTimeZone
*/
public function getTimezone()
{
return $this->timezone;
}
/**
* @return \DateTime
*/
public function getStartOfMonth()
{
$date = $this->createDateTime('first day of this month');
$date->setTime(0, 0, 0);
return $date;
}
/**
* @return \DateTime
*/
public function getEndOfMonth()
{
$date = $this->createDateTime('last day of this month');
$date->setTime(23, 59, 59);
return $date;
}
/**
* @param string $datetime
* @return \DateTime
*/
public function createDateTime(string $datetime = 'now')
{
$date = new \DateTime($datetime, $this->timezone);
return $date;
}
}

View File

@@ -40,6 +40,7 @@ class DateExtensions extends \Twig_Extension
new TwigFilter('month_name', [$this, 'monthName']),
new TwigFilter('date_short', [$this, 'dateShort']),
new TwigFilter('date_time', [$this, 'dateTime']),
new TwigFilter('date_format', [$this, 'dateFormat']),
new TwigFilter('time', [$this, 'time']),
];
}
@@ -66,6 +67,16 @@ class DateExtensions extends \Twig_Extension
return date_format($date, $format);
}
/**
* @param DateTime $date
* @param string $format
* @return false|string
*/
public function dateFormat(DateTime $date, string $format)
{
return date_format($date, $format);
}
/**
* @param DateTime $date
* @return string