diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index e090dd37..42a728dd 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -13,6 +13,7 @@ use App\Event\DashboardEvent; use App\Widget\Type\AbstractContainer; use App\Widget\Type\AuthorizedWidget; use App\Widget\Type\CompoundRow; +use App\Widget\Type\UserWidget; use App\Widget\WidgetContainerInterface; use App\Widget\WidgetException; use App\Widget\WidgetService; @@ -31,15 +32,15 @@ class DashboardController extends AbstractController /** * @var EventDispatcherInterface */ - protected $eventDispatcher; + private $eventDispatcher; /** * @var WidgetService */ - protected $widgets; + private $widgets; /** * @var array */ - protected $dashboard; + private $dashboard; /** * @param EventDispatcherInterface $dispatcher @@ -58,8 +59,9 @@ class DashboardController extends AbstractController */ public function indexAction() { - $event = new DashboardEvent($this->getUser()); + $user = $this->getUser(); + $event = new DashboardEvent($user); foreach ($this->dashboard as $widgetRow) { if (empty($widgetRow['widgets'])) { continue; @@ -110,6 +112,10 @@ class DashboardController extends AbstractController $add = $tmp; } + if ($widget instanceof UserWidget) { + $widget->setUser($user); + } + if ($add) { $row->addWidget($widget); } diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index e6f35431..e603652f 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -21,7 +21,6 @@ use App\Form\UserTeamsType; use App\Repository\TeamRepository; use App\Repository\TimesheetRepository; use App\Utils\LocaleSettings; -use Doctrine\Common\Collections\ArrayCollection; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\FormInterface; @@ -212,13 +211,6 @@ class ProfileController extends AbstractController $event = new PrepareUserEvent($profile); $this->dispatcher->dispatch($event); - /** @var \ArrayIterator $iterator */ - $iterator = $profile->getPreferences()->getIterator(); - $iterator->uasort(function (UserPreference $a, UserPreference $b) { - return ($a->getOrder() < $b->getOrder()) ? -1 : 1; - }); - $profile->setPreferences(new ArrayCollection(iterator_to_array($iterator))); - $original = []; foreach ($profile->getPreferences() as $preference) { $original[$preference->getName()] = $preference; @@ -227,48 +219,59 @@ class ProfileController extends AbstractController $form = $this->createPreferencesForm($profile); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $entityManager = $this->getDoctrine()->getManager(); - $preferences = $profile->getPreferences(); + if ($form->isSubmitted()) { + if ($form->isValid()) { + $entityManager = $this->getDoctrine()->getManager(); + $preferences = $profile->getPreferences(); - // do not allow to add unknown preferences - foreach ($preferences as $preference) { - if (!isset($original[$preference->getName()])) { - $preferences->removeElement($preference); + // do not allow to add unknown preferences + foreach ($preferences as $preference) { + if (!isset($original[$preference->getName()])) { + $preferences->removeElement($preference); + } } - } - // but allow to delete already saved settings - foreach ($original as $name => $preference) { - if (false === $profile->getPreferences()->contains($preference)) { - $entityManager->remove($preference); + // but allow to delete already saved settings + foreach ($original as $name => $preference) { + if (false === $profile->getPreferences()->contains($preference)) { + $entityManager->remove($preference); + } } + + $profile->setPreferences($preferences); + $entityManager->persist($profile); + $entityManager->flush(); + + $this->flashSuccess('action.update.success'); + + // switch locale ONLY if updated profile is the current user + $locale = $request->getLocale(); + if ($this->getUser()->getId() === $profile->getId()) { + $locale = $profile->getPreferenceValue('language', $locale); + } + + return $this->redirectToRoute('user_profile_preferences', [ + '_locale' => $locale, + 'username' => $profile->getUsername() + ]); + } else { + $this->flashError('action.update.error', ['%reason%' => 'Validation failed']); } - - $profile->setPreferences($preferences); - $entityManager->persist($profile); - $entityManager->flush(); - - $this->flashSuccess('action.update.success'); - - // switch locale ONLY if updated profile is the current user - $locale = $request->getLocale(); - if ($this->getUser()->getId() === $profile->getId()) { - $locale = $profile->getPreferenceValue('language', $locale); - } - - return $this->redirectToRoute('user_profile_preferences', [ - '_locale' => $locale, - 'username' => $profile->getUsername() - ]); } + // prepare ordered preferences $sections = []; + /** @var \ArrayIterator $iterator */ + $iterator = $profile->getPreferences()->getIterator(); + $iterator->uasort(function (UserPreference $a, UserPreference $b) { + return ($a->getOrder() < $b->getOrder()) ? -1 : 1; + }); + /** @var UserPreference $pref */ - foreach ($profile->getPreferences() as $pref) { + foreach ($iterator as $pref) { if ($pref->isEnabled()) { - $sections[$pref->getSection()] = $pref->getSection(); + $sections[$pref->getSection()][] = $pref->getName(); } } diff --git a/src/Entity/User.php b/src/Entity/User.php index bbb5211e..7de5fc9e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -336,7 +336,7 @@ class User extends BaseUser implements UserInterface } foreach ($this->preferences as $preference) { - if ($preference->getName() == $name) { + if ($preference->getName() === $name) { return $preference; } } diff --git a/src/Event/UserPreferenceEvent.php b/src/Event/UserPreferenceEvent.php index 0512faf0..45eabbe3 100644 --- a/src/Event/UserPreferenceEvent.php +++ b/src/Event/UserPreferenceEvent.php @@ -14,7 +14,7 @@ use App\Entity\UserPreference; use Symfony\Contracts\EventDispatcher\Event; /** - * This event should be used, if further user preferences should added dynamically + * This event should be used, if further user preferences should be added dynamically. */ final class UserPreferenceEvent extends Event { @@ -26,11 +26,11 @@ final class UserPreferenceEvent extends Event /** * @var User */ - protected $user; + private $user; /** * @var UserPreference[] */ - protected $preferences; + private $preferences = []; /** * @param User $user @@ -44,9 +44,10 @@ final class UserPreferenceEvent extends Event /** * Do not set the preferences directly to the user object, but ONLY via addPreference() + * * @return User */ - public function getUser() + public function getUser(): User { return $this->user; } @@ -54,7 +55,7 @@ final class UserPreferenceEvent extends Event /** * @return UserPreference[] */ - public function getPreferences() + public function getPreferences(): array { return $this->preferences; } diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php index 89c20ab1..093b16e1 100644 --- a/src/EventSubscriber/UserPreferenceSubscriber.php +++ b/src/EventSubscriber/UserPreferenceSubscriber.php @@ -24,7 +24,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\TimezoneType; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraints\Range; @@ -38,19 +37,14 @@ class UserPreferenceSubscriber implements EventSubscriberInterface * @var AuthorizationCheckerInterface */ protected $voter; - /** - * @var TokenStorageInterface - */ - protected $storage; /** * @var FormConfiguration */ protected $formConfig; - public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage, AuthorizationCheckerInterface $voter, FormConfiguration $formConfig) + public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter, FormConfiguration $formConfig) { $this->eventDispatcher = $dispatcher; - $this->storage = $storage; $this->voter = $voter; $this->formConfig = $formConfig; } diff --git a/src/EventSubscriber/UserProfileSubscriber.php b/src/EventSubscriber/UserProfileSubscriber.php index b675d318..30eff558 100644 --- a/src/EventSubscriber/UserProfileSubscriber.php +++ b/src/EventSubscriber/UserProfileSubscriber.php @@ -17,31 +17,23 @@ use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -class UserProfileSubscriber implements EventSubscriberInterface +final class UserProfileSubscriber implements EventSubscriberInterface { /** * @var EventDispatcherInterface */ - protected $eventDispatcher; - + private $eventDispatcher; /** * @var TokenStorageInterface */ - protected $storage; + private $storage; - /** - * @param EventDispatcherInterface $dispatcher - * @param TokenStorageInterface $storage - */ public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage) { $this->eventDispatcher = $dispatcher; $this->storage = $storage; } - /** - * @return array - */ public static function getSubscribedEvents(): array { return [ @@ -49,10 +41,7 @@ class UserProfileSubscriber implements EventSubscriberInterface ]; } - /** - * @param KernelEvent $event - */ - public function prepareUserProfile(KernelEvent $event) + public function prepareUserProfile(KernelEvent $event): void { if (!$this->canHandleEvent($event)) { return; @@ -65,11 +54,7 @@ class UserProfileSubscriber implements EventSubscriberInterface $this->eventDispatcher->dispatch($event); } - /** - * @param KernelEvent $event - * @return bool - */ - protected function canHandleEvent(KernelEvent $event): bool + private function canHandleEvent(KernelEvent $event): bool { // Ignore sub-requests if (!$event->isMasterRequest()) { diff --git a/src/Form/Type/UserPreferenceType.php b/src/Form/Type/UserPreferenceType.php index e23a730e..8a568374 100644 --- a/src/Form/Type/UserPreferenceType.php +++ b/src/Form/Type/UserPreferenceType.php @@ -34,10 +34,6 @@ class UserPreferenceType extends AbstractType $this->translate = $translator; } - /** - * @param FormBuilderInterface $builder - * @param array $options - */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addEventListener( @@ -56,7 +52,7 @@ class UserPreferenceType extends AbstractType } $required = true; - if (CheckboxType::class == $preference->getType()) { + if (CheckboxType::class === $preference->getType()) { $required = false; } diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index 00f762f5..2518053f 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -416,7 +416,7 @@ class TimesheetRepository extends EntityRepository $newDateBegin = clone $endTmp; } - // make sure to exclude entries that are outside the requested timerange: + // make sure to exclude entries that are outside the requested time-range: // these entries can exist if you have long running entries that started before $begin // for statistical reasons we have to include everything between $begin and $end while // excluding everything that is outside of that range @@ -449,6 +449,7 @@ class TimesheetRepository extends EntityRepository . '_' . $result->getProject()->getId() . '_' . $result->getActivity()->getId() ; + if (!isset($results[$dateKey]['details'][$detailsId])) { $results[$dateKey]['details'][$detailsId] = [ 'project' => $result->getProject(), @@ -456,10 +457,10 @@ class TimesheetRepository extends EntityRepository 'duration' => 0, 'rate' => 0, ]; - - $results[$dateKey]['details'][$detailsId]['duration'] += $duration; - $results[$dateKey]['details'][$detailsId]['rate'] += $rate; } + + $results[$dateKey]['details'][$detailsId]['duration'] += $duration; + $results[$dateKey]['details'][$detailsId]['rate'] += $rate; } $beginTmp = $newDateBegin; diff --git a/src/Repository/WidgetRepository.php b/src/Repository/WidgetRepository.php index 614450cb..5bb88b10 100644 --- a/src/Repository/WidgetRepository.php +++ b/src/Repository/WidgetRepository.php @@ -9,10 +9,8 @@ namespace App\Repository; -use App\Entity\User; -use App\Security\CurrentUser; -use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\Counter; +use App\Widget\Type\SimpleStatisticChart; use App\Widget\Type\YearChart; use App\Widget\WidgetException; use App\Widget\WidgetInterface; @@ -25,29 +23,19 @@ class WidgetRepository /** * @var TimesheetRepository */ - protected $repository; + private $repository; /** * @var array */ - protected $widgets = []; + private $widgets = []; /** * @var array */ - protected $definitions = []; - /** - * @var User|null - */ - protected $user; + private $definitions = []; - /** - * @param TimesheetRepository $repository - * @param CurrentUser $user - * @param array $widgets - */ - public function __construct(TimesheetRepository $repository, CurrentUser $user, array $widgets) + public function __construct(TimesheetRepository $repository, array $widgets) { $this->repository = $repository; - $this->user = $user->getUser(); $this->definitions = array_merge($this->getDefaultWidgets(), $widgets); } @@ -89,12 +77,6 @@ class WidgetRepository */ protected function create(string $name, array $widget): WidgetInterface { - $user = $this->user; - $timezone = new \DateTimeZone($user->getTimezone()); - $begin = !empty($widget['begin']) ? new \DateTime($widget['begin'], $timezone) : null; - $end = !empty($widget['end']) ? new \DateTime($widget['end'], $timezone) : null; - $theUser = $widget['user'] ? $user : null; - if (!isset($widget['type'])) { @trigger_error('Using a widget definition without a "type" is deprecated', E_USER_DEPRECATED); $widget['type'] = Counter::class; @@ -104,30 +86,25 @@ class WidgetRepository throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetClassName)); } - /** @var AbstractWidgetType $model */ - $model = new $widgetClassName(); - if (!($model instanceof AbstractWidgetType)) { + /** @var SimpleStatisticChart $model */ + $model = new $widgetClassName($this->repository); + if (!($model instanceof SimpleStatisticChart)) { throw new WidgetException( sprintf( 'Widget type "%s" is not an instance of "%s"', $widgetClassName, - AbstractWidgetType::class + SimpleStatisticChart::class ) ); } - try { - $data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser); - } catch (\Exception $ex) { - throw new WidgetException( - 'Failed loading widget data: ' . $ex->getMessage() - ); - } - $model + ->setQuery($widget['query']) + ->setBegin($widget['begin']) + ->setEnd($widget['end']) ->setId($name) ->setTitle($widget['title']) - ->setData($data); + ; if ($widget['query'] == TimesheetRepository::STATS_QUERY_DURATION) { $model->setOption('dataType', 'duration'); diff --git a/src/Twig/EventExtensions.php b/src/Twig/EventExtensions.php index 1146d1f8..9c04df8d 100644 --- a/src/Twig/EventExtensions.php +++ b/src/Twig/EventExtensions.php @@ -9,75 +9,19 @@ namespace App\Twig; -use App\Entity\User; -use App\Event\ThemeEvent; -use App\Security\CurrentUser; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use App\Twig\Runtime\ThemeEventExtension; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class EventExtensions extends AbstractExtension { - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - /** - * @var User - */ - protected $user; - - /** - * @param EventDispatcherInterface $dispatcher - * @param CurrentUser $user - */ - public function __construct(EventDispatcherInterface $dispatcher, CurrentUser $user) - { - $this->eventDispatcher = $dispatcher; - $this->user = $user->getUser(); - } - /** * {@inheritdoc} */ public function getFunctions() { return [ - new TwigFunction('trigger', [$this, 'triggerEvent']), + new TwigFunction('trigger', [ThemeEventExtension::class, 'trigger']), ]; } - - /** - * @return EventDispatcherInterface - */ - protected function getDispatcher() - { - return $this->eventDispatcher; - } - - /** - * @param string $eventName - * - * @return bool - */ - protected function hasListener($eventName) - { - return $this->getDispatcher()->hasListeners($eventName); - } - - /** - * @param string $eventName - * @param mixed $payload - * @return ThemeEvent - */ - public function triggerEvent(string $eventName, $payload = null) - { - $themeEvent = new ThemeEvent($this->user, $payload); - - if ($this->hasListener($eventName)) { - $this->getDispatcher()->dispatch($themeEvent, $eventName); - } - - return $themeEvent; - } } diff --git a/src/Twig/MarkdownExtension.php b/src/Twig/MarkdownExtension.php index 05e3790f..e2076cc7 100644 --- a/src/Twig/MarkdownExtension.php +++ b/src/Twig/MarkdownExtension.php @@ -52,7 +52,7 @@ final class MarkdownExtension extends AbstractExtension /** * Transforms the entities comment (customer, project, activity ...) into HTML. * - * @param string $content + * @param string|null $content * @param bool $fullLength * @return string */ diff --git a/src/Twig/Runtime/ThemeEventExtension.php b/src/Twig/Runtime/ThemeEventExtension.php new file mode 100644 index 00000000..e824726d --- /dev/null +++ b/src/Twig/Runtime/ThemeEventExtension.php @@ -0,0 +1,49 @@ +eventDispatcher = $dispatcher; + $this->user = $user; + } + + /** + * @param string $eventName + * @param mixed|null $payload + * @return ThemeEvent + */ + public function trigger(string $eventName, $payload = null): ThemeEvent + { + $themeEvent = new ThemeEvent($this->user->getUser(), $payload); + + if ($this->eventDispatcher->hasListeners($eventName)) { + $this->eventDispatcher->dispatch($themeEvent, $eventName); + } + + return $themeEvent; + } +} diff --git a/src/Validator/Constraints/TimeFormatValidator.php b/src/Validator/Constraints/TimeFormatValidator.php index b6c2cbf7..b8ed544b 100644 --- a/src/Validator/Constraints/TimeFormatValidator.php +++ b/src/Validator/Constraints/TimeFormatValidator.php @@ -26,6 +26,10 @@ class TimeFormatValidator extends ConstraintValidator throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimeFormat'); } + if (null === $value || '' === $value) { + return; + } + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } diff --git a/src/Widget/Type/AbstractWidgetType.php b/src/Widget/Type/AbstractWidgetType.php index 07970b3f..fd16c4a6 100644 --- a/src/Widget/Type/AbstractWidgetType.php +++ b/src/Widget/Type/AbstractWidgetType.php @@ -30,7 +30,7 @@ abstract class AbstractWidgetType implements WidgetInterface */ protected $data; - public function setId(string $id): AbstractWidgetType + public function setId(string $id): self { $this->id = $id; @@ -42,7 +42,7 @@ abstract class AbstractWidgetType implements WidgetInterface return $this->id; } - public function setData($data): AbstractWidgetType + public function setData($data): self { $this->data = $data; @@ -58,7 +58,7 @@ abstract class AbstractWidgetType implements WidgetInterface return $this->data; } - public function setTitle(string $title): AbstractWidgetType + public function setTitle(string $title): self { $this->title = $title; @@ -70,7 +70,7 @@ abstract class AbstractWidgetType implements WidgetInterface return $this->title; } - public function setOptions(array $options): AbstractWidgetType + public function setOptions(array $options): self { foreach ($options as $key => $value) { $this->options[$key] = $value; diff --git a/src/Widget/Type/Counter.php b/src/Widget/Type/Counter.php index 81ace956..4d8ca532 100644 --- a/src/Widget/Type/Counter.php +++ b/src/Widget/Type/Counter.php @@ -9,10 +9,13 @@ namespace App\Widget\Type; -class Counter extends SimpleWidget +use App\Repository\TimesheetRepository; + +final class Counter extends SimpleStatisticChart { - public function __construct() + public function __construct(TimesheetRepository $repository) { + parent::__construct($repository); $this->setOption('dataType', 'int'); } } diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php index 8efb8414..b33c6c60 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -9,12 +9,13 @@ namespace App\Widget\Type; +use App\Entity\Activity; +use App\Entity\Project; +use App\Entity\User; use App\Repository\TimesheetRepository; -use App\Security\CurrentUser; -use App\Timesheet\UserDateTimeFactory; use DateTime; -class DailyWorkingTimeChart extends SimpleWidget +class DailyWorkingTimeChart extends SimpleWidget implements UserWidget { public const DEFAULT_CHART = 'bar'; @@ -22,27 +23,26 @@ class DailyWorkingTimeChart extends SimpleWidget * @var TimesheetRepository */ protected $repository; - /** - * @var UserDateTimeFactory - */ - private $dateTimeFactory; - public function __construct(TimesheetRepository $repository, CurrentUser $user, UserDateTimeFactory $dateTime) + public function __construct(TimesheetRepository $repository) { $this->repository = $repository; - $this->dateTimeFactory = $dateTime; $this->setId('DailyWorkingTimeChart'); $this->setTitle('stats.yourWorkingHours'); $this->setOptions([ 'begin' => 'monday this week 00:00:00', 'end' => 'sunday this week 23:59:59', 'color' => '', - 'user' => $user->getUser(), 'type' => self::DEFAULT_CHART, 'id' => '', ]); } + public function setUser(User $user): void + { + $this->setOption('user', $user); + } + public function getOptions(array $options = []): array { $options = parent::getOptions($options); @@ -63,18 +63,44 @@ class DailyWorkingTimeChart extends SimpleWidget $options = $this->getOptions($options); $user = $options['user']; + if (null === $user || !($user instanceof User)) { + throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class); + } + if ($options['begin'] instanceof DateTime) { $begin = $options['begin']; } else { - $begin = new DateTime($options['begin'], $this->dateTimeFactory->getTimezone()); + $begin = new DateTime($options['begin'], new \DateTimeZone($user->getTimezone())); } if ($options['end'] instanceof DateTime) { $end = $options['end']; } else { - $end = new DateTime($options['end'], $this->dateTimeFactory->getTimezone()); + $end = new DateTime($options['end'], new \DateTimeZone($user->getTimezone())); } - return $this->repository->getDailyStats($user, $begin, $end); + $activities = []; + $statistics = $this->repository->getDailyStats($user, $begin, $end); + + foreach ($statistics as $day) { + foreach ($day->getDetails() as $entry) { + /** @var Activity $activity */ + $activity = $entry['activity']; + /** @var Project $project */ + $project = $entry['project']; + + $id = $project->getId() . '_' . $activity->getId(); + + $activities[$id] = [ + 'activity' => $activity, + 'project' => $project, + ]; + } + } + + return [ + 'activities' => $activities, + 'data' => $statistics, + ]; } } diff --git a/src/Widget/Type/PaginatedWorkingTimeChart.php b/src/Widget/Type/PaginatedWorkingTimeChart.php index 3ee5c658..f5f931f9 100644 --- a/src/Widget/Type/PaginatedWorkingTimeChart.php +++ b/src/Widget/Type/PaginatedWorkingTimeChart.php @@ -9,37 +9,40 @@ namespace App\Widget\Type; +use App\Entity\User; use App\Repository\TimesheetRepository; -use App\Security\CurrentUser; -use App\Timesheet\UserDateTimeFactory; use DateTime; -final class PaginatedWorkingTimeChart extends SimpleWidget +final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget { /** * @var TimesheetRepository */ private $repository; - /** - * @var UserDateTimeFactory - */ - private $dateTimeFactory; - public function __construct(TimesheetRepository $repository, CurrentUser $user, UserDateTimeFactory $dateTime) + public function __construct(TimesheetRepository $repository) { $this->repository = $repository; - $this->dateTimeFactory = $dateTime; $this->setId('PaginatedWorkingTimeChart'); $this->setTitle('stats.yourWorkingHours'); $this->setOptions([ - 'year' => (new DateTime('now', $this->dateTimeFactory->getTimezone()))->format('Y'), - 'week' => (new DateTime('now', $this->dateTimeFactory->getTimezone()))->format('W'), - 'user' => $user->getUser(), + 'year' => (new DateTime('now'))->format('Y'), + 'week' => (new DateTime('now'))->format('W'), 'type' => 'bar', ]); } + public function setUser(User $user): void + { + $this->setOption('user', $user); + $now = new DateTime('now', new \DateTimeZone($user->getTimezone())); + $this->setOptions([ + 'year' => $now->format('Y'), + 'week' => $now->format('W'), + ]); + } + public function getOptions(array $options = []): array { $options = parent::getOptions($options); @@ -51,9 +54,9 @@ final class PaginatedWorkingTimeChart extends SimpleWidget return $options; } - private function getDate($year, $week, $day, $hour, $minute, $second) + private function getDate(\DateTimeZone $timezone, $year, $week, $day, $hour, $minute, $second) { - $now = new DateTime('now', $this->dateTimeFactory->getTimezone()); + $now = new DateTime('now', $timezone); $now->setISODate($year, $week, $day); $now->setTime($hour, $minute, $second); @@ -63,10 +66,16 @@ final class PaginatedWorkingTimeChart extends SimpleWidget public function getData(array $options = []) { $options = $this->getOptions($options); - $user = $options['user']; - $weekBegin = $this->getDate($options['year'], $options['week'], 1, 0, 0, 0); - $weekEnd = $this->getDate($options['year'], $options['week'], 7, 23, 59, 59); + $user = $options['user']; + if (null === $user || !($user instanceof User)) { + throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class); + } + + $timezone = new \DateTimeZone($user->getTimezone()); + + $weekBegin = $this->getDate($timezone, $options['year'], $options['week'], 1, 0, 0, 0); + $weekEnd = $this->getDate($timezone, $options['year'], $options['week'], 7, 23, 59, 59); return [ 'begin' => clone $weekBegin, @@ -74,8 +83,8 @@ final class PaginatedWorkingTimeChart extends SimpleWidget 'stats' => $this->repository->getDailyStats($user, $weekBegin, $weekEnd), 'day' => $this->repository->getStatistic( 'duration', - new DateTime('00:00:00', $this->dateTimeFactory->getTimezone()), - new DateTime('23:59:59', $this->dateTimeFactory->getTimezone()), + new DateTime('00:00:00', $timezone), + new DateTime('23:59:59', $timezone), $user ), 'week' => $this->repository->getStatistic( @@ -92,8 +101,8 @@ final class PaginatedWorkingTimeChart extends SimpleWidget ), 'year' => $this->repository->getStatistic( 'duration', - new DateTime(sprintf('01 january %s 00:00:00', $options['year']), $this->dateTimeFactory->getTimezone()), - new DateTime(sprintf('31 december %s 23:59:59', $options['year']), $this->dateTimeFactory->getTimezone()), + new DateTime(sprintf('01 january %s 00:00:00', $options['year']), $timezone), + new DateTime(sprintf('31 december %s 23:59:59', $options['year']), $timezone), $user ), ]; diff --git a/src/Widget/Type/SimpleStatisticChart.php b/src/Widget/Type/SimpleStatisticChart.php new file mode 100644 index 00000000..0354fdea --- /dev/null +++ b/src/Widget/Type/SimpleStatisticChart.php @@ -0,0 +1,101 @@ +repository = $repository; + } + + public function setQuery(string $query): SimpleStatisticChart + { + $this->query = $query; + + return $this; + } + + public function setBegin(?string $begin): SimpleStatisticChart + { + $this->begin = $begin; + + return $this; + } + + public function setEnd(?string $end): SimpleStatisticChart + { + $this->end = $end; + + return $this; + } + + public function setUser(User $user): SimpleStatisticChart + { + $this->user = $user; + + return $this; + } + + public function setData($data): AbstractWidgetType + { + throw new \InvalidArgumentException('Cannot set data on instances of SimpleStatisticChart'); + } + + /** + * @param array $options + * @return mixed|null + * @throws WidgetException + */ + public function getData(array $options = []) + { + $timezone = date_default_timezone_get(); + if (null !== $this->user) { + $timezone = $this->user->getTimezone(); + } + $timezone = new \DateTimeZone($timezone); + + $begin = !empty($this->begin) ? new \DateTime($this->begin, $timezone) : null; + $end = !empty($this->end) ? new \DateTime($this->end, $timezone) : null; + + try { + return $this->repository->getStatistic($this->query, $begin, $end, $this->user); + } catch (\Exception $ex) { + throw new WidgetException( + 'Failed loading widget data: ' . $ex->getMessage() + ); + } + } +} diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php index 8e24f665..7c82b080 100644 --- a/src/Widget/Type/UserTeamProjects.php +++ b/src/Widget/Type/UserTeamProjects.php @@ -13,23 +13,19 @@ use App\Entity\Project; use App\Entity\Team; use App\Entity\User; use App\Repository\ProjectRepository; -use App\Security\CurrentUser; -class UserTeamProjects extends SimpleWidget implements AuthorizedWidget +class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWidget { /** * @var ProjectRepository */ private $repository; - public function __construct(CurrentUser $user, ProjectRepository $repository) + public function __construct(ProjectRepository $repository) { $this->setId('UserTeamProjects'); $this->setTitle('label.my_team_projects'); - $this->setOptions([ - 'user' => $user->getUser(), - 'id' => '', - ]); + $this->setOption('id', ''); $this->repository = $repository; } @@ -80,4 +76,9 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget { return ['budget_team_project', 'budget_teamlead_project', 'budget_project']; } + + public function setUser(User $user): void + { + $this->setOption('user', $user); + } } diff --git a/src/Widget/Type/UserTeams.php b/src/Widget/Type/UserTeams.php index 557ac217..2cfd4d19 100644 --- a/src/Widget/Type/UserTeams.php +++ b/src/Widget/Type/UserTeams.php @@ -10,18 +10,14 @@ namespace App\Widget\Type; use App\Entity\User; -use App\Security\CurrentUser; -class UserTeams extends SimpleWidget implements AuthorizedWidget +class UserTeams extends SimpleWidget implements AuthorizedWidget, UserWidget { - public function __construct(CurrentUser $user) + public function __construct() { $this->setId('UserTeams'); $this->setTitle('label.my_teams'); - $this->setOptions([ - 'user' => $user->getUser(), - 'id' => '', - ]); + $this->setOption('id', ''); } public function getOptions(array $options = []): array @@ -51,4 +47,9 @@ class UserTeams extends SimpleWidget implements AuthorizedWidget { return ['view_team_member', 'view_team']; } + + public function setUser(User $user): void + { + $this->setOption('user', $user); + } } diff --git a/src/Widget/Type/UserWidget.php b/src/Widget/Type/UserWidget.php new file mode 100644 index 00000000..742fc3f3 --- /dev/null +++ b/src/Widget/Type/UserWidget.php @@ -0,0 +1,22 @@ +repository = $repository; } - /** - * @param string $widget - * @return bool - */ public function hasWidget(string $widget): bool { return $this->repository->has($widget); diff --git a/templates/reporting/actions.html.twig b/templates/reporting/actions.html.twig index 5ba21434..2906dd7a 100644 --- a/templates/reporting/actions.html.twig +++ b/templates/reporting/actions.html.twig @@ -6,6 +6,7 @@ {% set children = children|merge({(report.id): {'title': report.label|trans({}, 'reporting'), 'url': path(report.route), 'class': 'toolbar-action report-' ~ report.id}}) %} {% endfor %} {% set actions = actions|merge({'reporting': {'children': children}}) %} + {% set actions = actions|merge({'help': {'url': 'reporting.html'|docu_link, 'target': '_blank'}}) %} {% set event = trigger('actions.reporting', {'actions': actions}) %} {{ widgets.page_actions(event.payload.actions) }} {% endmacro %} diff --git a/templates/user/form.html.twig b/templates/user/form.html.twig index 3a75585c..9eb24948 100644 --- a/templates/user/form.html.twig +++ b/templates/user/form.html.twig @@ -2,11 +2,11 @@ {% block main %} {{ form_start(form) }} - {% for section, counter in sections %} + {% for section, entries in sections %} {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} {% block box_body %} {% for pref in form.children.preferences %} - {% if pref.vars.data.section == section %} + {% if pref.vars.data.name in entries %} {{ form_row(pref) }} {% endif %} {% endfor %} diff --git a/templates/widget/paginatedworkingtimechart.html.twig b/templates/widget/paginatedworkingtimechart.html.twig index 83656ec3..801e06f3 100644 --- a/templates/widget/paginatedworkingtimechart.html.twig +++ b/templates/widget/paginatedworkingtimechart.html.twig @@ -1 +1 @@ -{{ render_widget('PaginatedWorkingTimeChart', {'year': year, 'week': week}) }} +{{ render_widget('PaginatedWorkingTimeChart', {'user': user, 'year': year, 'week': week}) }} diff --git a/templates/widget/widget-dailyworkingtimechart.html.twig b/templates/widget/widget-dailyworkingtimechart.html.twig index a2efaa16..252122e1 100644 --- a/templates/widget/widget-dailyworkingtimechart.html.twig +++ b/templates/widget/widget-dailyworkingtimechart.html.twig @@ -1,16 +1,9 @@ {% set type = options.type|default('bar') %} {% set chart_id = options.id %} {% set backgroundColor = kimai_context.chart.background_color %} -{% set borderColor = kimai_context.chart.border_color %} {% set gridColor = kimai_context.chart.grid_color %} -{% set colors = options.color|default('')|split(';') %} -{% if colors.0 is defined and not colors.0 is empty %} - {% set backgroundColor = colors.0 %} - {% set borderColor = colors.0 %} - {% if colors.1 is defined and not colors.1 is empty %} - {% set borderColor = colors.1 %} - {% endif %} -{% endif %} +{% set activities = data.activities %} +{% set data = data.data %}
@@ -31,22 +24,50 @@ {%- endfor %} ], datasets: [ + {% for activityId, activity in activities -%} + {% set activityColor = activity.activity|color|default(activity.project|color|default(backgroundColor)) %} + {% set activityName = activity.activity.name %} { - backgroundColor: '{{ backgroundColor }}', - borderColor: '{{ borderColor }}', + label: '{{ activityName }}', + backgroundColor: '{{ activityColor }}', + borderColor: '#fff', + borderWidth: 1, data: [ - {% for day in data -%} - {{ (day.totalDuration / 3600)|number_format(2, '.', '') }} - {%- if not loop.last %},{% endif -%} - {%- endfor %} + {%- for day in data -%} + {% set realDayData = null %} + {%- for entry in day.details -%} + {% set loopId = (entry.project.id ~ '_' ~ entry.activity.id) %} + {%- if loopId == activityId -%} + {% set realDayData = (entry.duration / 3600)|number_format(2, '.', '') %} + {%- endif -%} + {%- endfor -%} + {%- if realDayData is not null -%} + '{{ realDayData }}' + {%- else -%} + 0 + {%- endif -%} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} ], realData: [ - {% for day in data -%} - '{{ day.totalDuration|duration }}' - {%- if not loop.last %},{% endif -%} - {%- endfor %} + {%- for day in data -%} + {% set realDayData = null %} + {%- for entry in day.details -%} + {% set loopId = (entry.project.id ~ '_' ~ entry.activity.id) %} + {%- if loopId == activityId -%} + {% set realDayData = {duration: entry.duration|duration, project: entry.project.name, customer: entry.project.customer.name, activity: entry.activity.name, total: day.totalDuration|duration} %} + {%- endif -%} + {%- endfor -%} + {%- if realDayData is not null -%} + {{ realDayData|json_encode|raw }} + {%- else -%} + 0 + {%- endif -%} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} ] - } + }, + {%- endfor %} ] }, options: { @@ -57,11 +78,13 @@ categoryPercentage: 0.9, scales: { xAxes: [{ + stacked: true, gridLines: { display: false }, }], yAxes: [{ + stacked: true, ticks: { beginAtZero: true }, @@ -70,14 +93,36 @@ color: '{{ gridColor }}', lineWidth: 1 } - }] + }], }, tooltips: { callbacks: { label: function(tooltipItem, data) { - return data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index]; - } - } + var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index]; + return ' ' + tooltipData.duration + ': ' + tooltipData.activity; + }, + beforeTitle: function(tooltipItems, data) { + var tooltipItem = tooltipItems[0]; + var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index]; + return tooltipData.customer; + }, + title: function(tooltipItems, data) { + var tooltipItem = tooltipItems[0]; + var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index]; + return tooltipData.project; + }, + afterTitle: function(tooltipItems, data) { + return ' '; + }, + footer: function(tooltipItems, data) { + var tooltipItem = tooltipItems[0]; + var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index]; + return '{{ 'stats.durationTotal'|trans }}: ' + tooltipData.total; + }, + beforeFooter: function(tooltipItems, data) { + return ' '; + }, + }, } } } diff --git a/tests/Controller/ProfileControllerTest.php b/tests/Controller/ProfileControllerTest.php index f1b1d3ea..b6e84407 100644 --- a/tests/Controller/ProfileControllerTest.php +++ b/tests/Controller/ProfileControllerTest.php @@ -410,10 +410,14 @@ class ProfileControllerTest extends ControllerBaseTest return [ // assert that the user doesn't have the "hourly-rate_own_profile" permission [User::ROLE_USER, UserFixtures::USERNAME_USER, 82, 82, 'ar', null], - // admins are allowed to update their own hourly rate + // teamleads are allowed to update their own hourly rate, but not other peoples hourly rate + [User::ROLE_TEAMLEAD, UserFixtures::USERNAME_TEAMLEAD, 35, 37.5, 'ar', 19.54], + // admins are allowed to update their own hourly rate, but not other peoples hourly rate [User::ROLE_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'ar', 19.54], - // admins are allowed to update other peoples hourly rate - [User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_USER, 82, 37.5, 'en', 19.54], + // super-admins are allowed to update other peoples hourly rate + [User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'en', 19.54], + // super-admins are allowed to update their own hourly rate + [User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, 46, 37.5, 'ar', 19.54], ]; } @@ -431,21 +435,16 @@ class ProfileControllerTest extends ControllerBaseTest $this->assertEquals($hourlyRateOriginal, $user->getPreferenceValue(UserPreference::HOURLY_RATE)); $this->assertNull($user->getPreferenceValue(UserPreference::INTERNAL_RATE)); $this->assertNull($user->getPreferenceValue(UserPreference::SKIN)); - $this->assertEquals(false, $user->getPreferenceValue('theme.collapsed_sidebar')); - $this->assertEquals('month', $user->getPreferenceValue('calendar.initial_view')); $form = $client->getCrawler()->filter('form[name=user_preferences_form]')->form(); $client->submit($form, [ 'user_preferences_form' => [ 'preferences' => [ - ['name' => UserPreference::HOURLY_RATE, 'value' => 37.5], - ['name' => UserPreference::INTERNAL_RATE, 'value' => 19.54], - ['name' => 'timezone', 'value' => 'America/Creston'], - ['name' => 'language', 'value' => 'ar'], - ['name' => UserPreference::SKIN, 'value' => 'blue'], - ['name' => 'theme.layout', 'value' => 'fixed'], - ['name' => 'theme.collapsed_sidebar', 'value' => true], - ['name' => 'calendar.initial_view', 'value' => 'agendaDay'], + 0 => ['name' => UserPreference::HOURLY_RATE, 'value' => 37.5], + 1 => ['name' => UserPreference::INTERNAL_RATE, 'value' => 19.54], + 2 => ['name' => UserPreference::TIMEZONE, 'value' => 'America/Creston'], + 3 => ['name' => UserPreference::LOCALE, 'value' => 'ar'], + 4 => ['name' => UserPreference::SKIN, 'value' => 'blue'], ] ] ]); @@ -462,10 +461,11 @@ class ProfileControllerTest extends ControllerBaseTest $this->assertEquals($hourlyRate, $user->getPreferenceValue(UserPreference::HOURLY_RATE)); $this->assertEquals($expectedInternalRate, $user->getPreferenceValue(UserPreference::INTERNAL_RATE)); - $this->assertEquals('', $user->getPreferenceValue('America/Creston')); - $this->assertEquals('ar', $user->getPreferenceValue('language')); + $this->assertEquals('America/Creston', $user->getPreferenceValue(UserPreference::TIMEZONE)); + $this->assertEquals('America/Creston', $user->getTimezone()); + $this->assertEquals('ar', $user->getPreferenceValue(UserPreference::LOCALE)); + $this->assertEquals('ar', $user->getLanguage()); + $this->assertEquals('ar', $user->getLocale()); $this->assertEquals('blue', $user->getPreferenceValue(UserPreference::SKIN)); - $this->assertEquals(true, $user->getPreferenceValue('theme.collapsed_sidebar')); - $this->assertEquals('agendaDay', $user->getPreferenceValue('calendar.initial_view')); } } diff --git a/tests/Controller/SystemConfigurationControllerTest.php b/tests/Controller/SystemConfigurationControllerTest.php index dc5ffb7c..cd1180a8 100644 --- a/tests/Controller/SystemConfigurationControllerTest.php +++ b/tests/Controller/SystemConfigurationControllerTest.php @@ -353,10 +353,8 @@ class SystemConfigurationControllerTest extends ControllerBaseTest [ '#system_configuration_form_calendar_configuration_2_value', '#system_configuration_form_calendar_configuration_3_value', - '#system_configuration_form_calendar_configuration_3_value', '#system_configuration_form_calendar_configuration_4_value', '#system_configuration_form_calendar_configuration_5_value', - '#system_configuration_form_calendar_configuration_5_value', ], true ); diff --git a/tests/Controller/WidgetControllerTest.php b/tests/Controller/WidgetControllerTest.php new file mode 100644 index 00000000..a21660f5 --- /dev/null +++ b/tests/Controller/WidgetControllerTest.php @@ -0,0 +1,34 @@ +assertUrlIsSecured('/widgets/working-time/2020/1'); + } + + public function testWorkingtimechartAction() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/widgets/working-time/2020/1'); + + $content = $client->getResponse()->getContent(); + self::assertStringContainsString('id="PaginatedWorkingTimeChart"', $content); + self::assertStringContainsString('myChart = new Chart', $content); + self::assertStringContainsString("KimaiPaginatedBoxWidget.create('#PaginatedWorkingTimeChart');", $content); + } +} diff --git a/tests/EventSubscriber/UserPreferenceSubscriberTest.php b/tests/EventSubscriber/UserPreferenceSubscriberTest.php index ccd6fa38..17f8d0d1 100644 --- a/tests/EventSubscriber/UserPreferenceSubscriberTest.php +++ b/tests/EventSubscriber/UserPreferenceSubscriberTest.php @@ -16,7 +16,6 @@ use App\Event\PrepareUserEvent; use App\EventSubscriber\UserPreferenceSubscriber; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; /** @@ -88,9 +87,8 @@ class UserPreferenceSubscriberTest extends TestCase $authMock->expects($this->once())->method('isGranted')->willReturn($seeHourlyRate); $eventMock = $this->createMock(EventDispatcherInterface::class); - $tokenMock = $this->createMock(TokenStorageInterface::class); $formConfigMock = $this->createMock(FormConfiguration::class); - return new UserPreferenceSubscriber($eventMock, $tokenMock, $authMock, $formConfigMock); + return new UserPreferenceSubscriber($eventMock, $authMock, $formConfigMock); } } diff --git a/tests/Repository/WidgetRepositoryTest.php b/tests/Repository/WidgetRepositoryTest.php index 636959c1..70756a5e 100644 --- a/tests/Repository/WidgetRepositoryTest.php +++ b/tests/Repository/WidgetRepositoryTest.php @@ -9,10 +9,8 @@ namespace App\Tests\Repository; -use App\Entity\User; use App\Repository\TimesheetRepository; use App\Repository\WidgetRepository; -use App\Tests\Mocks\Security\CurrentUserFactory; use App\Widget\Type\CompoundChart; use App\Widget\Type\Counter; use App\Widget\WidgetException; @@ -26,9 +24,8 @@ class WidgetRepositoryTest extends TestCase public function testHasWidget() { $repoMock = $this->createMock(TimesheetRepository::class); - $userMock = (new CurrentUserFactory($this))->create(new User()); - $sut = new WidgetRepository($repoMock, $userMock, ['test' => []]); + $sut = new WidgetRepository($repoMock, ['test' => []]); $this->assertFalse($sut->has('foo')); $this->assertTrue($sut->has('test')); @@ -40,9 +37,8 @@ class WidgetRepositoryTest extends TestCase $this->expectExceptionMessage('Cannot find widget "foo".'); $repoMock = $this->createMock(TimesheetRepository::class); - $userMock = (new CurrentUserFactory($this))->create(new User()); - $sut = new WidgetRepository($repoMock, $userMock, ['test' => []]); + $sut = new WidgetRepository($repoMock, ['test' => []]); $sut->get('foo'); } @@ -52,21 +48,19 @@ class WidgetRepositoryTest extends TestCase $this->expectExceptionMessage('Unknown widget type "FooBar"'); $repoMock = $this->createMock(TimesheetRepository::class); - $userMock = (new CurrentUserFactory($this))->create(new User()); - $sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => 'FooBar', 'user' => false]]); + $sut = new WidgetRepository($repoMock, ['test' => ['type' => 'FooBar', 'user' => false]]); $sut->get('test'); } public function testGetWidgetTriggersExceptionOnWrongClass() { $this->expectException(WidgetException::class); - $this->expectExceptionMessage('Widget type "App\Widget\Type\CompoundChart" is not an instance of "App\Widget\Type\AbstractWidgetType"'); + $this->expectExceptionMessage('Widget type "App\Widget\Type\CompoundChart" is not an instance of "App\Widget\Type\SimpleStatisticChart"'); $repoMock = $this->createMock(TimesheetRepository::class); - $userMock = (new CurrentUserFactory($this))->create(new User()); - $sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => CompoundChart::class, 'user' => false]]); + $sut = new WidgetRepository($repoMock, ['test' => ['type' => CompoundChart::class, 'user' => false]]); $sut->get('test'); } @@ -78,8 +72,6 @@ class WidgetRepositoryTest extends TestCase $repoMock = $this->createMock(TimesheetRepository::class); $repoMock->method('getStatistic')->willReturn($data); - $userMock = (new CurrentUserFactory($this))->create(new User()); - $widget = [ 'color' => 'sunny', 'icon' => 'far fa-test', @@ -91,7 +83,7 @@ class WidgetRepositoryTest extends TestCase 'type' => Counter::class, ]; - $sut = new WidgetRepository($repoMock, $userMock, ['test' => $widget]); + $sut = new WidgetRepository($repoMock, ['test' => $widget]); $widget = $sut->get('test'); $options = $widget->getOptions(); diff --git a/tests/Twig/EventExtensionsTest.php b/tests/Twig/EventExtensionsTest.php new file mode 100644 index 00000000..b45928ae --- /dev/null +++ b/tests/Twig/EventExtensionsTest.php @@ -0,0 +1,39 @@ +getSut(); + $twigFunctions = $sut->getFunctions(); + self::assertCount(\count($functions), $twigFunctions); + $i = 0; + /** @var TwigFunction $filter */ + foreach ($twigFunctions as $filter) { + self::assertInstanceOf(TwigFunction::class, $filter); + self::assertEquals($functions[$i++], $filter->getName()); + } + } +} diff --git a/tests/Twig/Runtime/ThemeEventExtensionTest.php b/tests/Twig/Runtime/ThemeEventExtensionTest.php new file mode 100644 index 00000000..8f8e70df --- /dev/null +++ b/tests/Twig/Runtime/ThemeEventExtensionTest.php @@ -0,0 +1,48 @@ +createMock(EventDispatcherInterface::class); + $dispatcher->expects($this->once())->method('hasListeners')->willReturn($hasListener); + $dispatcher->expects($hasListener ? $this->once() : $this->never())->method('dispatch'); + + $user = (new CurrentUserFactory($this))->create(new User()); + + return new ThemeEventExtension($dispatcher, $user); + } + + public function testTrigger() + { + $sut = $this->getSut(); + $event = $sut->trigger('foo', []); + self::assertInstanceOf(ThemeEvent::class, $event); + } + + public function testTriggerWithoutListener() + { + $sut = $this->getSut(false); + $event = $sut->trigger('foo', []); + self::assertInstanceOf(ThemeEvent::class, $event); + } +} diff --git a/tests/Twig/WidgetExtensionTest.php b/tests/Twig/WidgetExtensionTest.php index c1b7b555..3e20872f 100644 --- a/tests/Twig/WidgetExtensionTest.php +++ b/tests/Twig/WidgetExtensionTest.php @@ -10,7 +10,7 @@ namespace App\Tests\Twig; use App\Twig\WidgetExtension; -use App\Widget\Type\Counter; +use App\Widget\Type\More; use App\Widget\WidgetInterface; use App\Widget\WidgetRendererInterface; use App\Widget\WidgetService; @@ -72,7 +72,7 @@ class WidgetExtensionTest extends TestCase public function testRenderWidgetByString() { - $widget = new Counter(); + $widget = new More(); $sut = $this->getSut(true, $widget, new TestRenderer()); $options = ['foo' => 'bar', 'dataType' => 'blub']; $result = $sut->renderWidget('test', $options); @@ -82,7 +82,7 @@ class WidgetExtensionTest extends TestCase public function testRenderWidgetObject() { - $widget = new Counter(); + $widget = new More(); $sut = $this->getSut(null, null, new TestRenderer()); $options = ['foo' => 'bar', 'dataType' => 'blub']; $result = $sut->renderWidget($widget, $options); diff --git a/tests/Validator/Constraints/TimeFormatValidatorTest.php b/tests/Validator/Constraints/TimeFormatValidatorTest.php index c7e8d92f..e42d1a77 100644 --- a/tests/Validator/Constraints/TimeFormatValidatorTest.php +++ b/tests/Validator/Constraints/TimeFormatValidatorTest.php @@ -44,7 +44,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase /** * @dataProvider getValidTimes */ - public function testValidationSucceeds(string $value) + public function testValidationSucceeds(?string $value) { $this->validator->validate($value, new TimeFormat()); $this->assertNoViolation(); @@ -53,6 +53,8 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase public function getValidTimes() { return [ + [''], + [null], ['00:00'], ['00:01'], ['23:00'], @@ -65,7 +67,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase /** * @dataProvider getInvalidTimes */ - public function testValidationProblem(string $value) + public function testValidationProblem(?string $value) { $this->validator->validate($value, new TimeFormat()); @@ -78,6 +80,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase public function getInvalidTimes() { return [ + ['a'], ['1:00'], ['01:1'], ['00:60'], diff --git a/tests/Widget/Renderer/CompoundChartRendererTest.php b/tests/Widget/Renderer/CompoundChartRendererTest.php index 3435dd78..6ff9c853 100644 --- a/tests/Widget/Renderer/CompoundChartRendererTest.php +++ b/tests/Widget/Renderer/CompoundChartRendererTest.php @@ -12,7 +12,7 @@ namespace App\Tests\Widget\Renderer; use App\Widget\Renderer\CompoundChartRenderer; use App\Widget\Type\CompoundChart; use App\Widget\Type\CompoundRow; -use App\Widget\Type\Counter; +use App\Widget\Type\More; use PHPUnit\Framework\TestCase; use Twig\Environment; @@ -40,7 +40,7 @@ class CompoundChartRendererTest extends TestCase $sut = new CompoundChartRenderer($twig); $row = new CompoundChart(); $row->setTitle('foo-bar'); - $row->addWidget(new Counter()); + $row->addWidget(new More()); $result = $sut->render($row); $result = json_decode($result, true); diff --git a/tests/Widget/Renderer/CompoundRowRendererTest.php b/tests/Widget/Renderer/CompoundRowRendererTest.php index f24f892f..1a18ddb4 100644 --- a/tests/Widget/Renderer/CompoundRowRendererTest.php +++ b/tests/Widget/Renderer/CompoundRowRendererTest.php @@ -12,7 +12,7 @@ namespace App\Tests\Widget\Renderer; use App\Widget\Renderer\CompoundRowRenderer; use App\Widget\Type\CompoundChart; use App\Widget\Type\CompoundRow; -use App\Widget\Type\Counter; +use App\Widget\Type\More; use PHPUnit\Framework\TestCase; use Twig\Environment; @@ -40,7 +40,7 @@ class CompoundRowRendererTest extends TestCase $sut = new CompoundRowRenderer($twig); $row = new CompoundRow(); $row->setTitle('foo-bar'); - $row->addWidget(new Counter()); + $row->addWidget(new More()); $result = $sut->render($row); $result = json_decode($result, true); diff --git a/tests/Widget/Renderer/SimpleWidgetRendererTest.php b/tests/Widget/Renderer/SimpleWidgetRendererTest.php index c26653c8..7bbb8f59 100644 --- a/tests/Widget/Renderer/SimpleWidgetRendererTest.php +++ b/tests/Widget/Renderer/SimpleWidgetRendererTest.php @@ -10,7 +10,6 @@ namespace App\Tests\Widget\Renderer; use App\Widget\Renderer\SimpleWidgetRenderer; -use App\Widget\Type\Counter; use App\Widget\Type\More; use App\Widget\Type\SimpleWidget; use PHPUnit\Framework\TestCase; @@ -60,7 +59,6 @@ class SimpleWidgetRendererTest extends TestCase { return [ [new SimpleWidget(), 'widget/widget-simplewidget.html.twig', 'yellow'], - [new Counter(), 'widget/widget-counter.html.twig', 'asdfgh'], [new More(), 'widget/widget-more.html.twig', '#123456'], ]; } diff --git a/tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php b/tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php new file mode 100644 index 00000000..77890821 --- /dev/null +++ b/tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php @@ -0,0 +1,26 @@ +createSut(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart'); + + $sut->setData(10); + } +} diff --git a/tests/Widget/Type/AbstractWidgetTypeTest.php b/tests/Widget/Type/AbstractWidgetTypeTest.php index cdeed4af..c3fe7a22 100644 --- a/tests/Widget/Type/AbstractWidgetTypeTest.php +++ b/tests/Widget/Type/AbstractWidgetTypeTest.php @@ -38,7 +38,6 @@ abstract class AbstractWidgetTypeTest extends TestCase self::assertInstanceOf(AbstractWidgetType::class, $sut->setOptions([])); self::assertInstanceOf(AbstractWidgetType::class, $sut->setId('')); self::assertInstanceOf(AbstractWidgetType::class, $sut->setTitle('')); - self::assertInstanceOf(AbstractWidgetType::class, $sut->setData('')); } public function testSetter() @@ -57,8 +56,14 @@ abstract class AbstractWidgetTypeTest extends TestCase // id $sut->setId('cvbnmyx'); self::assertEquals('cvbnmyx', $sut->getId()); + } + + public function testData() + { + $sut = $this->createSut(); + + self::assertInstanceOf(AbstractWidgetType::class, $sut->setData('')); - // data $sut->setData('slkudfhalksjdhfkljsahdf'); self::assertEquals('slkudfhalksjdhfkljsahdf', $sut->getData()); diff --git a/tests/Widget/Type/CounterTest.php b/tests/Widget/Type/CounterTest.php index fb8527e8..b887e6f0 100644 --- a/tests/Widget/Type/CounterTest.php +++ b/tests/Widget/Type/CounterTest.php @@ -9,19 +9,24 @@ namespace App\Tests\Widget\Type; +use App\Repository\TimesheetRepository; use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\Counter; use App\Widget\Type\SimpleWidget; /** * @covers \App\Widget\Type\Counter + * @covers \App\Widget\Type\SimpleStatisticChart * @covers \App\Widget\Type\SimpleWidget */ -class CounterTest extends AbstractWidgetTypeTest +class CounterTest extends AbstractSimpleStatisticsWidgetTypeTest { public function createSut(): AbstractWidgetType { - return new Counter(); + $sut = new Counter($this->createMock(TimesheetRepository::class)); + $sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE); + + return $sut; } public function getDefaultOptions(): array @@ -37,7 +42,8 @@ class CounterTest extends AbstractWidgetTypeTest public function testTemplateName() { - $sut = new Counter(); + /** @var Counter $sut */ + $sut = $this->createSut(); self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName()); } } diff --git a/tests/Widget/Type/DailyWorkingTimeChartTest.php b/tests/Widget/Type/DailyWorkingTimeChartTest.php index cbb21706..f24b7630 100644 --- a/tests/Widget/Type/DailyWorkingTimeChartTest.php +++ b/tests/Widget/Type/DailyWorkingTimeChartTest.php @@ -9,11 +9,11 @@ namespace App\Tests\Widget\Type; +use App\Entity\Activity; +use App\Entity\Project; use App\Entity\User; use App\Model\Statistic\Day; use App\Repository\TimesheetRepository; -use App\Tests\Mocks\Security\CurrentUserFactory; -use App\Tests\Mocks\Security\UserDateTimeFactoryFactory; use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\DailyWorkingTimeChart; use App\Widget\Type\SimpleWidget; @@ -30,11 +30,11 @@ class DailyWorkingTimeChartTest extends TestCase public function createSut(): AbstractWidgetType { $repository = $this->createMock(TimesheetRepository::class); - $mockFactory = new UserDateTimeFactoryFactory($this); - $userFactory = new CurrentUserFactory($this); - $user = $userFactory->create(new User(), 'Europe/Berlin'); - return new DailyWorkingTimeChart($repository, $user, $mockFactory->create('Europe/Berlin')); + $sut = new DailyWorkingTimeChart($repository); + $sut->setUser(new User()); + + return $sut; } public function testExtendsSimpleWidget() @@ -100,22 +100,38 @@ class DailyWorkingTimeChartTest extends TestCase public function testGetData() { + $activity = $this->createMock(Activity::class); + $activity->method('getId')->willReturn(42); + + $project = $this->createMock(Project::class); + $project->method('getId')->willReturn(4711); + $repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->onlyMethods(['getDailyData'])->getMock(); - $repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) { + $repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) use ($activity, $project) { return [ - ['year' => $begin->format('Y'), 'month' => $begin->format('n'), 'day' => $begin->format('j'), 'rate' => 13.75, 'duration' => 1234, 'details' => []] + ['year' => $begin->format('Y'), 'month' => $begin->format('n'), 'day' => $begin->format('j'), 'rate' => 13.75, 'duration' => 1234, 'details' => [ + ['activity' => $activity, 'project' => $project] + ]] ]; }); - $userFactory = new CurrentUserFactory($this); - $user = $userFactory->create(new User(), 'Europe/Berlin'); - - $mockFactory = new UserDateTimeFactoryFactory($this); - - $sut = new DailyWorkingTimeChart($repository, $user, $mockFactory->create('Europe/Berlin')); + $sut = new DailyWorkingTimeChart($repository); + $sut->setUser(new User()); $data = $sut->getData([]); - self::assertCount(7, $data); - foreach ($data as $statObj) { + self::assertCount(2, $data); + self::assertArrayHasKey('activities', $data); + self::assertArrayHasKey('data', $data); + + self::assertCount(1, $data['activities']); + self::assertArrayHasKey('4711_42', $data['activities']); + self::assertCount(2, $data['activities']['4711_42']); + self::assertArrayHasKey('activity', $data['activities']['4711_42']); + self::assertArrayHasKey('project', $data['activities']['4711_42']); + self::assertSame($activity, $data['activities']['4711_42']['activity']); + self::assertSame($project, $data['activities']['4711_42']['project']); + + self::assertCount(7, $data['data']); + foreach ($data['data'] as $statObj) { self::assertInstanceOf(Day::class, $statObj); } } diff --git a/tests/Widget/Type/YearChartTest.php b/tests/Widget/Type/YearChartTest.php index 4b41b865..03bfd935 100644 --- a/tests/Widget/Type/YearChartTest.php +++ b/tests/Widget/Type/YearChartTest.php @@ -9,17 +9,23 @@ namespace App\Tests\Widget\Type; +use App\Repository\TimesheetRepository; use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\YearChart; /** * @covers \App\Widget\Type\YearChart + * @covers \App\Widget\Type\SimpleStatisticChart + * @covers \App\Widget\Type\SimpleWidget */ -class YearChartTest extends AbstractWidgetTypeTest +class YearChartTest extends AbstractSimpleStatisticsWidgetTypeTest { public function createSut(): AbstractWidgetType { - return new YearChart(); + $sut = new YearChart($this->createMock(TimesheetRepository::class)); + $sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE); + + return $sut; } public function getDefaultOptions(): array