From bb4233ddfe2c75c62205873d4abd86a07ed0df03 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 16 Aug 2020 01:43:38 +0200 Subject: [PATCH 01/12] use stacked bars in dashboard to show different working tasks per day --- src/Widget/Type/DailyWorkingTimeChart.php | 26 +++++- .../widget-dailyworkingtimechart.html.twig | 93 ++++++++++++++----- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php index 8efb8414..9e73af88 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -9,6 +9,8 @@ namespace App\Widget\Type; +use App\Entity\Activity; +use App\Entity\Project; use App\Repository\TimesheetRepository; use App\Security\CurrentUser; use App\Timesheet\UserDateTimeFactory; @@ -75,6 +77,28 @@ class DailyWorkingTimeChart extends SimpleWidget $end = new DateTime($options['end'], $this->dateTimeFactory->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/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 ' '; + }, + }, } } } From 34a1e078243a41bf70837a95ca051996106d699e Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 16 Aug 2020 01:50:40 +0200 Subject: [PATCH 02/12] adapt test --- tests/Widget/Type/DailyWorkingTimeChartTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Widget/Type/DailyWorkingTimeChartTest.php b/tests/Widget/Type/DailyWorkingTimeChartTest.php index cbb21706..6989842d 100644 --- a/tests/Widget/Type/DailyWorkingTimeChartTest.php +++ b/tests/Widget/Type/DailyWorkingTimeChartTest.php @@ -114,8 +114,12 @@ class DailyWorkingTimeChartTest extends TestCase $sut = new DailyWorkingTimeChart($repository, $user, $mockFactory->create('Europe/Berlin')); $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(7, $data['data']); + foreach ($data['data'] as $statObj) { self::assertInstanceOf(Day::class, $statObj); } } From ed4b82f4b18077306fb9b5f186ff9b2023928511 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 16 Aug 2020 12:20:33 +0200 Subject: [PATCH 03/12] Weekly reporting view (#1892) --- src/Constants.php | 4 + src/Controller/AbstractController.php | 20 ++- src/Controller/ReportingController.php | 156 ++++++++++++++--- src/Entity/ColorTrait.php | 5 + src/Form/Type/ColorPickerType.php | 3 +- src/Form/Type/MonthPickerType.php | 32 +--- src/Form/Type/WeekPickerType.php | 69 ++++++++ src/Reporting/DateByUser.php | 48 ++++++ src/Reporting/MonthByUser.php | 36 +--- src/Reporting/MonthByUserForm.php | 11 +- src/Reporting/MonthlyUserListForm.php | 11 +- src/Reporting/WeekByUser.php | 14 ++ src/Reporting/WeekByUserForm.php | 63 +++++++ src/Repository/TimesheetRepository.php | 6 +- src/Timesheet/DateTimeFactory.php | 101 +++++++++++ src/Timesheet/UserDateTimeFactory.php | 61 ++----- src/Twig/DateExtensions.php | 30 ++-- src/Twig/Extensions.php | 33 ++++ src/Twig/ReportingExtension.php | 1 + templates/form/kimai-theme.html.twig | 34 ++-- templates/reporting/month_by_user.html.twig | 35 ++-- .../reporting/monthly_user_list.html.twig | 18 +- templates/reporting/week_by_user.html.twig | 112 +++++++++++++ tests/Controller/ReportingControllerTest.php | 51 +++++- tests/Entity/ActivityTest.php | 4 + tests/Entity/CustomerTest.php | 4 + tests/Entity/ProjectTest.php | 4 + tests/Reporting/AbstractDateByUserTest.php | 43 +++++ tests/Reporting/MonthByUserTest.php | 25 +++ tests/Reporting/WeekByUserTest.php | 25 +++ tests/Timesheet/DateTimeFactoryTest.php | 158 ++++++++++++++++++ tests/Timesheet/UserDateTimeFactoryTest.php | 63 +------ tests/Twig/DateExtensionsTest.php | 21 ++- tests/Twig/ExtensionsTest.php | 40 ++++- tests/Twig/ReportingExtensionTest.php | 2 +- translations/messages.nl.xlf | 4 + translations/reporting.de.xlf | 4 + translations/reporting.en.xlf | 4 + translations/reporting.nl.xlf | 23 +++ 39 files changed, 1130 insertions(+), 248 deletions(-) create mode 100644 src/Form/Type/WeekPickerType.php create mode 100644 src/Reporting/DateByUser.php create mode 100644 src/Reporting/WeekByUser.php create mode 100644 src/Reporting/WeekByUserForm.php create mode 100644 src/Timesheet/DateTimeFactory.php create mode 100644 templates/reporting/week_by_user.html.twig create mode 100644 tests/Reporting/AbstractDateByUserTest.php create mode 100644 tests/Reporting/MonthByUserTest.php create mode 100644 tests/Reporting/WeekByUserTest.php create mode 100644 tests/Timesheet/DateTimeFactoryTest.php create mode 100644 translations/reporting.nl.xlf diff --git a/src/Constants.php b/src/Constants.php index c6e82c13..6b5da059 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -42,4 +42,8 @@ class Constants * Application wide default locale. */ public const DEFAULT_LOCALE = 'en'; + /** + * Default color for Customer, Project and Activity entities + */ + public const DEFAULT_COLOR = '#d2d6de'; } diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index f365ea01..d23ec533 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -9,7 +9,10 @@ namespace App\Controller; +use App\Configuration\LanguageFormattings; use App\Entity\User; +use App\Timesheet\DateTimeFactory; +use App\Utils\LocaleFormats; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController; use Symfony\Component\Translation\DataCollectorTranslator; @@ -108,7 +111,22 @@ abstract class AbstractController extends BaseAbstractController implements Serv { return array_merge(parent::getSubscribedServices(), [ 'translator' => TranslatorInterface::class, - 'logger' => LoggerInterface::class + 'logger' => LoggerInterface::class, + LanguageFormattings::class => LanguageFormattings::class, ]); } + + protected function getDateTimeFactory(?User $user = null): DateTimeFactory + { + if (null === $user) { + $user = $this->getUser(); + } + + return new DateTimeFactory(new \DateTimeZone($user->getTimezone())); + } + + protected function getLocaleFormats(string $locale): LocaleFormats + { + return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale); + } } diff --git a/src/Controller/ReportingController.php b/src/Controller/ReportingController.php index 4a80e779..f75206c8 100644 --- a/src/Controller/ReportingController.php +++ b/src/Controller/ReportingController.php @@ -14,12 +14,15 @@ use App\Reporting\MonthByUser; use App\Reporting\MonthByUserForm; use App\Reporting\MonthlyUserList; use App\Reporting\MonthlyUserListForm; +use App\Reporting\WeekByUser; +use App\Reporting\WeekByUserForm; use App\Repository\Query\UserQuery; use App\Repository\TimesheetRepository; use App\Repository\UserRepository; -use App\Timesheet\UserDateTimeFactory; +use Exception; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; @@ -39,46 +42,79 @@ final class ReportingController extends AbstractController * @var UserRepository */ private $userRepository; - /** - * @var UserDateTimeFactory - */ - private $dateTimeFactory; - public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository, UserDateTimeFactory $dateTimeFactory) + public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository) { $this->timesheetRepository = $timesheetRepository; $this->userRepository = $userRepository; - $this->dateTimeFactory = $dateTimeFactory; } /** * @Route(path="/", name="reporting", methods={"GET"}) - * @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"}) + * + * @return Response */ - public function monthByUser(Request $request) + public function defaultReport(): Response { - $user = $this->getUser(); + return $this->redirectToRoute('report_user_week'); + } + + private function canSelectUser(): bool + { + if (!$this->isGranted('view_other_timesheet')) { + return false; + } + + $currentUser = $this->getUser(); + + if ($currentUser->canSeeAllData()) { + return true; + } + + if ($currentUser->hasTeamAssignment()) { + return true; + } + + return false; + } + + /** + * @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"}) + * + * @param Request $request + * @return Response + * @throws Exception + */ + public function monthByUser(Request $request): Response + { + $currentUser = $this->getUser(); + $dateTimeFactory = $this->getDateTimeFactory($currentUser); + $localeFormats = $this->getLocaleFormats($request->getLocale()); + $canChangeUser = $this->canSelectUser(); $values = new MonthByUser(); - $values->setUser($user); - $values->setDate($this->dateTimeFactory->getStartOfMonth()); + $values->setUser($currentUser); + $values->setDate($dateTimeFactory->getStartOfMonth()); $form = $this->createForm(MonthByUserForm::class, $values, [ - 'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(), + 'include_user' => $canChangeUser, + 'timezone' => $dateTimeFactory->getTimezone()->getName(), + 'start_date' => $values->getDate(), + 'format' => $localeFormats->getDateTypeFormat(), ]); $form->submit($request->query->all(), false); if ($values->getUser() === null) { - $values->setUser($user); + $values->setUser($currentUser); } - if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) { + if ($currentUser !== $values->getUser() && !$canChangeUser) { throw new AccessDeniedException('User is not allowed to see other users timesheet'); } if ($values->getDate() === null) { - $values->setDate($this->dateTimeFactory->getStartOfMonth()); + $values->setDate($dateTimeFactory->getStartOfMonth()); } $start = $values->getDate(); @@ -110,12 +146,82 @@ final class ReportingController extends AbstractController } /** - * @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"}) - * @Security("is_granted('view_other_timesheet')") + * @Route(path="/week_by_user", name="report_user_week", methods={"GET","POST"}) + * + * @param Request $request + * @return Response + * @throws Exception */ - public function montlyhUsersList(Request $request) + public function weekByUser(Request $request): Response { $currentUser = $this->getUser(); + $dateTimeFactory = $this->getDateTimeFactory($currentUser); + $localeFormats = $this->getLocaleFormats($request->getLocale()); + $canChangeUser = $this->canSelectUser(); + + $values = new WeekByUser(); + $values->setUser($currentUser); + $values->setDate($dateTimeFactory->getStartOfWeek()); + + $form = $this->createForm(WeekByUserForm::class, $values, [ + 'include_user' => $canChangeUser, + 'timezone' => $dateTimeFactory->getTimezone()->getName(), + 'start_date' => $values->getDate(), + 'format' => $localeFormats->getDateTypeFormat(), + ]); + + $form->submit($request->query->all(), false); + + if ($values->getUser() === null) { + $values->setUser($currentUser); + } + + if ($currentUser !== $values->getUser() && !$canChangeUser) { + throw new AccessDeniedException('User is not allowed to see other users timesheet'); + } + + if ($values->getDate() === null) { + $values->setDate($dateTimeFactory->getStartOfWeek()); + } + + $start = $dateTimeFactory->getStartOfWeek($values->getDate()); + $end = $dateTimeFactory->getEndOfWeek($values->getDate()); + + $selectedUser = $values->getUser(); + + $previous = clone $start; + $previous->modify('-1 week'); + + $next = clone $start; + $next->modify('+1 week'); + + $data = $this->timesheetRepository->getDailyStats($selectedUser, $start, $end); + $rows = $this->prepareMonthlyData($data); + + return $this->render('reporting/week_by_user.html.twig', [ + 'form' => $form->createView(), + 'days' => $data, + 'rows' => $rows, + 'user' => $selectedUser, + 'current' => $start, + 'next' => $next, + 'previous' => $previous, + ]); + } + + /** + * @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"}) + * @Security("is_granted('view_other_timesheet')") + * + * @param Request $request + * @return Response + * @throws Exception + */ + public function monthlyUsersList(Request $request): Response + { + $currentUser = $this->getUser(); + $dateTimeFactory = $this->getDateTimeFactory(); + $localeFormats = $this->getLocaleFormats($request->getLocale()); $query = new UserQuery(); $query->setCurrentUser($currentUser); @@ -124,18 +230,22 @@ final class ReportingController extends AbstractController $rows = []; $values = new MonthlyUserList(); - $values->setDate($this->dateTimeFactory->getStartOfMonth()); + $values->setDate($dateTimeFactory->getStartOfMonth()); - $form = $this->createForm(MonthlyUserListForm::class, $values, []); + $form = $this->createForm(MonthlyUserListForm::class, $values, [ + 'timezone' => $dateTimeFactory->getTimezone()->getName(), + 'start_date' => $values->getDate(), + 'format' => $localeFormats->getDateTypeFormat(), + ]); $form->submit($request->query->all(), false); if ($form->isSubmitted() && !$form->isValid()) { - $values->setDate($this->dateTimeFactory->getStartOfMonth()); + $values->setDate($dateTimeFactory->getStartOfMonth()); } if ($values->getDate() === null) { - $values->setDate($this->dateTimeFactory->getStartOfMonth()); + $values->setDate($dateTimeFactory->getStartOfMonth()); } $start = $values->getDate(); diff --git a/src/Entity/ColorTrait.php b/src/Entity/ColorTrait.php index 2a0f7ecc..c9bfae16 100644 --- a/src/Entity/ColorTrait.php +++ b/src/Entity/ColorTrait.php @@ -9,6 +9,7 @@ namespace App\Entity; +use App\Constants; use App\Export\Annotation as Exporter; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -36,6 +37,10 @@ trait ColorTrait */ public function getColor(): ?string { + if ($this->color === Constants::DEFAULT_COLOR) { + return null; + } + return $this->color; } diff --git a/src/Form/Type/ColorPickerType.php b/src/Form/Type/ColorPickerType.php index 57296f84..ecfe0de8 100644 --- a/src/Form/Type/ColorPickerType.php +++ b/src/Form/Type/ColorPickerType.php @@ -9,6 +9,7 @@ namespace App\Form\Type; +use App\Constants; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Extension\Core\Type\ColorType; @@ -17,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class ColorPickerType extends AbstractType implements DataTransformerInterface { - public const DEFAULT_COLOR = '#d2d6de'; + public const DEFAULT_COLOR = Constants::DEFAULT_COLOR; /** * {@inheritdoc} diff --git a/src/Form/Type/MonthPickerType.php b/src/Form/Type/MonthPickerType.php index 35b3a21a..d4311dde 100644 --- a/src/Form/Type/MonthPickerType.php +++ b/src/Form/Type/MonthPickerType.php @@ -9,8 +9,6 @@ namespace App\Form\Type; -use App\Timesheet\UserDateTimeFactory; -use App\Utils\LocaleSettings; use App\Utils\MomentFormatConverter; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -25,38 +23,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ final class MonthPickerType extends AbstractType { - /** - * @var LocaleSettings - */ - private $localeSettings; - - /** - * @var UserDateTimeFactory - */ - private $dateTime; - - public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime) - { - $this->localeSettings = $localeSettings; - $this->dateTime = $dateTime; - } - /** * {@inheritdoc} */ public function configureOptions(OptionsResolver $resolver) { - $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, + 'format' => DateType::HTML5_FORMAT, + 'start_date' => new \DateTime(), ]); } @@ -66,13 +42,13 @@ final class MonthPickerType extends AbstractType $date = $form->getData(); if (null === $date) { - $date = $this->dateTime->getStartOfMonth(); + $date = $options['start_date']; } $view->vars['month'] = $date; $view->vars['previousMonth'] = (clone $date)->modify('-1 month'); $view->vars['nextMonth'] = (clone $date)->modify('+1 month'); - $view->vars['momentFormat'] = (new MomentFormatConverter())->convert($this->localeSettings->getDateTypeFormat()); + $view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']); } /** diff --git a/src/Form/Type/WeekPickerType.php b/src/Form/Type/WeekPickerType.php new file mode 100644 index 00000000..f2ff139a --- /dev/null +++ b/src/Form/Type/WeekPickerType.php @@ -0,0 +1,69 @@ +setDefaults([ + 'widget' => 'single_text', + 'html5' => false, + 'format' => DateType::HTML5_FORMAT, + 'start_date' => new \DateTime(), + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + /** @var \DateTime|null $date */ + $date = $form->getData(); + + if (null === $date) { + $date = $options['start_date']; + } + + $view->vars['week'] = $date; + $view->vars['previousWeek'] = (clone $date)->modify('-1 week'); + $view->vars['nextWeek'] = (clone $date)->modify('+1 week'); + $view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return DateType::class; + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'weekpicker'; + } +} diff --git a/src/Reporting/DateByUser.php b/src/Reporting/DateByUser.php new file mode 100644 index 00000000..41ac9318 --- /dev/null +++ b/src/Reporting/DateByUser.php @@ -0,0 +1,48 @@ +user; + } + + public function setUser(User $user): self + { + $this->user = $user; + + return $this; + } + + public function getDate(): ?\DateTime + { + return $this->date; + } + + public function setDate(\DateTime $date): self + { + $this->date = $date; + + return $this; + } +} diff --git a/src/Reporting/MonthByUser.php b/src/Reporting/MonthByUser.php index 719bdb0e..978029a4 100644 --- a/src/Reporting/MonthByUser.php +++ b/src/Reporting/MonthByUser.php @@ -9,40 +9,6 @@ namespace App\Reporting; -use App\Entity\User; - -final class MonthByUser +final class MonthByUser extends DateByUser { - /** - * @var User - */ - private $user; - /** - * @var \DateTime - */ - private $date; - - public function getUser(): ?User - { - return $this->user; - } - - public function setUser(User $user): MonthByUser - { - $this->user = $user; - - return $this; - } - - public function getDate(): ?\DateTime - { - return $this->date; - } - - public function setDate(\DateTime $date): MonthByUser - { - $this->date = $date; - - return $this; - } } diff --git a/src/Reporting/MonthByUserForm.php b/src/Reporting/MonthByUserForm.php index d64f3b50..36b384e1 100644 --- a/src/Reporting/MonthByUserForm.php +++ b/src/Reporting/MonthByUserForm.php @@ -12,6 +12,7 @@ namespace App\Reporting; use App\Form\Type\MonthPickerType; use App\Form\Type\UserType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -32,7 +33,12 @@ class MonthByUserForm extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('date', MonthPickerType::class); + $builder->add('date', MonthPickerType::class, [ + 'model_timezone' => $options['timezone'], + 'view_timezone' => $options['timezone'], + 'start_date' => $options['start_date'], + 'format' => $options['format'], + ]); if ($options['include_user']) { $builder->add('user', UserType::class, ['width' => false]); @@ -46,6 +52,9 @@ class MonthByUserForm extends AbstractType { $resolver->setDefaults([ 'data_class' => MonthByUser::class, + 'timezone' => date_default_timezone_get(), + 'start_date' => new \DateTime(), + 'format' => DateType::HTML5_FORMAT, 'include_user' => false, 'csrf_protection' => false, 'method' => 'GET', diff --git a/src/Reporting/MonthlyUserListForm.php b/src/Reporting/MonthlyUserListForm.php index 7fffeae1..b83600b5 100644 --- a/src/Reporting/MonthlyUserListForm.php +++ b/src/Reporting/MonthlyUserListForm.php @@ -11,6 +11,7 @@ namespace App\Reporting; use App\Form\Type\MonthPickerType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -31,7 +32,12 @@ class MonthlyUserListForm extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('date', MonthPickerType::class); + $builder->add('date', MonthPickerType::class, [ + 'model_timezone' => $options['timezone'], + 'view_timezone' => $options['timezone'], + 'start_date' => $options['start_date'], + 'format' => $options['format'], + ]); } /** @@ -41,6 +47,9 @@ class MonthlyUserListForm extends AbstractType { $resolver->setDefaults([ 'data_class' => MonthlyUserList::class, + 'timezone' => date_default_timezone_get(), + 'start_date' => new \DateTime(), + 'format' => DateType::HTML5_FORMAT, 'csrf_protection' => false, 'method' => 'GET', ]); diff --git a/src/Reporting/WeekByUser.php b/src/Reporting/WeekByUser.php new file mode 100644 index 00000000..b2c3f91e --- /dev/null +++ b/src/Reporting/WeekByUser.php @@ -0,0 +1,14 @@ +add('date', WeekPickerType::class, [ + 'model_timezone' => $options['timezone'], + 'view_timezone' => $options['timezone'], + 'start_date' => $options['start_date'], + 'format' => $options['format'], + ]); + + if ($options['include_user']) { + $builder->add('user', UserType::class, ['width' => false]); + } + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => WeekByUser::class, + 'timezone' => date_default_timezone_get(), + 'start_date' => new \DateTime(), + 'format' => DateType::HTML5_FORMAT, + 'include_user' => false, + 'csrf_protection' => false, + 'method' => 'GET', + ]); + } +} diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index 559b7c95..00f762f5 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -444,7 +444,11 @@ class TimesheetRepository extends EntityRepository $results[$dateKey]['rate'] += $rate; $results[$dateKey]['duration'] += $duration; - $detailsId = $result->getProject()->getCustomer()->getId() . '_' . $result->getProject()->getId(); + $detailsId = + $result->getProject()->getCustomer()->getId() + . '_' . $result->getProject()->getId() + . '_' . $result->getActivity()->getId() + ; if (!isset($results[$dateKey]['details'][$detailsId])) { $results[$dateKey]['details'][$detailsId] = [ 'project' => $result->getProject(), diff --git a/src/Timesheet/DateTimeFactory.php b/src/Timesheet/DateTimeFactory.php new file mode 100644 index 00000000..3c037f73 --- /dev/null +++ b/src/Timesheet/DateTimeFactory.php @@ -0,0 +1,101 @@ +setTimezone($timezone); + } + + public function setTimezone(DateTimeZone $timezone) + { + $this->timezone = $timezone; + } + + public function getTimezone(): DateTimeZone + { + return $this->timezone; + } + + public function getStartOfMonth(): DateTime + { + $date = $this->createDateTime('first day of this month'); + $date->setTime(0, 0, 0); + + return $date; + } + + public function getStartOfWeek(?DateTime $date = null): DateTime + { + if (null === $date) { + $date = $this->createDateTime('now'); + } + + return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 1, 0, 0, 0); + } + + public function getEndOfWeek(?DateTime $date = null): DateTime + { + if (null === $date) { + $date = $this->createDateTime('now'); + } + + return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 7, 23, 59, 59); + } + + public function getEndOfMonth(): DateTime + { + $date = $this->createDateTime('last day of this month'); + $date->setTime(23, 59, 59); + + return $date; + } + + private function createWeekDateTime($year, $week, $day, $hour, $minute, $second) + { + $date = new DateTime('now', $this->getTimezone()); + $date->setISODate($year, $week, $day); + $date->setTime($hour, $minute, $second); + + return $date; + } + + public function createDateTime(string $datetime = 'now'): DateTime + { + $date = new DateTime($datetime, $this->getTimezone()); + + return $date; + } + + /** + * @param string $format + * @param null|string $datetime + * @return bool|DateTime + */ + public function createDateTimeFromFormat(string $format, ?string $datetime = 'now') + { + $date = DateTime::createFromFormat($format, $datetime, $this->getTimezone()); + + return $date; + } +} diff --git a/src/Timesheet/UserDateTimeFactory.php b/src/Timesheet/UserDateTimeFactory.php index 021e21d3..e4843be6 100644 --- a/src/Timesheet/UserDateTimeFactory.php +++ b/src/Timesheet/UserDateTimeFactory.php @@ -11,26 +11,31 @@ namespace App\Timesheet; use App\Entity\User; use App\Security\CurrentUser; +use DateTimeZone; -class UserDateTimeFactory +/** + * @internal use DateTimeFactory instead: this one relies on the global context and will be deprecated in the future + */ +class UserDateTimeFactory extends DateTimeFactory { - /** - * @var \DateTimeZone - */ - private $timezone; /** * @var CurrentUser */ private $user; + /** + * @var bool + */ + private $initializedFromUser = false; public function __construct(CurrentUser $user) { + parent::__construct(null); $this->user = $user; } - public function getTimezone(): \DateTimeZone + public function getTimezone(): DateTimeZone { - if (null === $this->timezone) { + if ($this->initializedFromUser === false) { $timezone = date_default_timezone_get(); $user = $this->user->getUser(); @@ -38,44 +43,12 @@ class UserDateTimeFactory $timezone = $user->getTimezone(); } - $this->timezone = new \DateTimeZone($timezone); + $timezone = new DateTimeZone($timezone); + + parent::setTimezone($timezone); + $this->initializedFromUser = true; } - return $this->timezone; - } - - public function getStartOfMonth(): \DateTime - { - $date = $this->createDateTime('first day of this month'); - $date->setTime(0, 0, 0); - - return $date; - } - - public function getEndOfMonth(): \DateTime - { - $date = $this->createDateTime('last day of this month'); - $date->setTime(23, 59, 59); - - return $date; - } - - public function createDateTime(string $datetime = 'now'): \DateTime - { - $date = new \DateTime($datetime, $this->getTimezone()); - - return $date; - } - - /** - * @param string $format - * @param null|string $datetime - * @return bool|\DateTime - */ - public function createDateTimeFromFormat(string $format, ?string $datetime = 'now') - { - $date = \DateTime::createFromFormat($format, $datetime, $this->getTimezone()); - - return $date; + return parent::getTimezone(); } } diff --git a/src/Twig/DateExtensions.php b/src/Twig/DateExtensions.php index 7180e4b1..35f75ad8 100644 --- a/src/Twig/DateExtensions.php +++ b/src/Twig/DateExtensions.php @@ -230,34 +230,36 @@ class DateExtensions extends AbstractExtension return $date->format($this->timeFormat); } - public function monthName(\DateTime $dateTime): string + /** + * @see https://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats + * @see http://userguide.icu-project.org/formatparse/datetime + * + * @param DateTime $dateTime + * @param string $format + * @return string + */ + private function formatIntl(\DateTime $dateTime, string $format): string { - // @see http://userguide.icu-project.org/formatparse/datetime $formatter = new \IntlDateFormatter( $this->locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, $dateTime->getTimezone()->getName(), \IntlDateFormatter::GREGORIAN, - 'LLLL' + $format ); return $formatter->format($dateTime); } + public function monthName(\DateTime $dateTime, bool $withYear = false): string + { + return $this->formatIntl($dateTime, ($withYear ? 'LLLL yyyy' : 'LLLL')); + } + public function dayName(\DateTime $dateTime, bool $short = false): string { - // @see http://userguide.icu-project.org/formatparse/datetime - $formatter = new \IntlDateFormatter( - $this->locale, - \IntlDateFormatter::FULL, - \IntlDateFormatter::FULL, - $dateTime->getTimezone()->getName(), - \IntlDateFormatter::GREGORIAN, - $short ? 'EE' : 'EEEE' - ); - - return $formatter->format($dateTime); + return $this->formatIntl($dateTime, ($short ? 'EE' : 'EEEE')); } /** diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php index 99cddd05..1fe95fa8 100644 --- a/src/Twig/Extensions.php +++ b/src/Twig/Extensions.php @@ -10,6 +10,10 @@ namespace App\Twig; use App\Constants; +use App\Entity\Activity; +use App\Entity\Customer; +use App\Entity\EntityWithMetaFields; +use App\Entity\Project; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -27,6 +31,7 @@ class Extensions extends AbstractExtension return [ new TwigFilter('docu_link', [$this, 'documentationLink']), new TwigFilter('multiline_indent', [$this, 'multilineIndent']), + new TwigFilter('color', [$this, 'color']), ]; } @@ -40,6 +45,34 @@ class Extensions extends AbstractExtension ]; } + public function color(EntityWithMetaFields $entity): ?string + { + if ($entity instanceof Activity) { + if (!empty($entity->getColor())) { + return $entity->getColor(); + } + + if (null !== $entity->getProject()) { + $entity = $entity->getProject(); + } + } + + if ($entity instanceof Project) { + if (!empty($entity->getColor())) { + return $entity->getColor(); + } + $entity = $entity->getCustomer(); + } + + if ($entity instanceof Customer) { + if (!empty($entity->getColor())) { + return $entity->getColor(); + } + } + + return null; + } + /** * @param object $object * @return null|string diff --git a/src/Twig/ReportingExtension.php b/src/Twig/ReportingExtension.php index 08ba3428..08a3e27c 100644 --- a/src/Twig/ReportingExtension.php +++ b/src/Twig/ReportingExtension.php @@ -54,6 +54,7 @@ final class ReportingExtension extends AbstractExtension $event = new ReportingEvent($user); if ($this->security->isGranted('view_reporting')) { + $event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week')); $event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month')); if ($this->security->isGranted('view_other_timesheet')) { $event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users')); diff --git a/templates/form/kimai-theme.html.twig b/templates/form/kimai-theme.html.twig index 30eaffc5..65e06e46 100644 --- a/templates/form/kimai-theme.html.twig +++ b/templates/form/kimai-theme.html.twig @@ -48,26 +48,30 @@ {% block monthpicker_widget -%} - - {% set type = 'hidden' %} - {{ block('form_widget_simple') }} + {{ block('hidden_widget') }} {%- endblock monthpicker_widget %} + +{% block weekpicker_widget -%} + + {{ block('hidden_widget') }} +{%- endblock weekpicker_widget %} diff --git a/templates/reporting/month_by_user.html.twig b/templates/reporting/month_by_user.html.twig index 1d63271a..04da63f9 100644 --- a/templates/reporting/month_by_user.html.twig +++ b/templates/reporting/month_by_user.html.twig @@ -4,6 +4,13 @@ {% block report %} + {% set hasData = false %} + {% for day in days %} + {% if day.details is not empty %} + {% set hasData = true %} + {% endif %} + {% endfor %} + {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} {% import "macros/widgets.html.twig" as widgets %} {% block box_before %} @@ -20,10 +27,14 @@ {% endif %} {{ form_widget(form.date) }} {% endblock %} - {% block box_body_class %}user-month-reporting-box no-padding table-responsive{% endblock %} + {% block box_body_class %}user-month-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %} {% block box_body %} + {% if not hasData %} + {{ widgets.nothing_found() }} + {% else %} + {% for day in days %} {% endfor %} - {% for project in rows %} + {% for day in project.days %} - {% endfor %} - {% for activity in project.activities %} + {% for day in activity.days %} - {% endfor %} - {% endfor %} {% endfor %} {% set total = 0 %} - {% for day in days %} - + + {% for day in days %} + {% endfor %} -
@@ -31,52 +42,54 @@ {{ day.day|date_format('d.m') }}
{{ widgets.label_project(project.project) }} {{ project.duration|duration }} + {% if day.duration > 0 %} - {{ day.duration|duration }} + {{ day.duration|duration }} {% endif %} {{ project.duration|duration }}
{{ widgets.label_activity(activity.activity) }} {{ activity.duration|duration }} + {% if day.duration > 0 %} {{ day.duration|duration }} {% endif %} {{ activity.duration|duration }}
+ {% set total = total + day.totalDuration %} + {% endfor %} + {{ total|duration }} {% if day.totalDuration > 0 %} {{ day.totalDuration|duration }} - {% set total = total + day.totalDuration %} {% endif %} {{ total|duration }}
+ {% endif %} {% endblock %} {% endembed %} diff --git a/templates/reporting/monthly_user_list.html.twig b/templates/reporting/monthly_user_list.html.twig index 3462e492..b82e641d 100644 --- a/templates/reporting/monthly_user_list.html.twig +++ b/templates/reporting/monthly_user_list.html.twig @@ -15,10 +15,11 @@ {% block box_title %} {{ form_widget(form.date) }} {% endblock %} - {% block box_body_class %}monthly-user-list-reporting-box no-padding table-responsive{% endblock %} + {% block box_body_class %}monthly-user-list-reporting-box table-responsive no-padding{% endblock %} {% block box_body %} + {% for day in days %} {% endfor %} - {% for userDay in rows %} {% set usersMonthDuration = 0 %} @@ -34,15 +34,25 @@ + {% for day in userDay.days %} + {% if day.totalDuration > 0 %} + {% set usersMonthDuration = usersMonthDuration + day.totalDuration %} + {% endif %} + {% endfor %} + {% for day in userDay.days %} {% endfor %} - {% endfor %}
@@ -26,7 +27,6 @@ {{ day|date_format('d.m') }}
{{ widgets.username(userDay.user) }} + {% if usersMonthDuration == 0 %} + - + {% else %} + {{ usersMonthDuration|duration }} + {% endif %} + {% if day.totalDuration > 0 %} {{ day.totalDuration|duration }} - {% set usersMonthDuration = usersMonthDuration + day.totalDuration %} {% endif %} {{ usersMonthDuration|duration }}
diff --git a/templates/reporting/week_by_user.html.twig b/templates/reporting/week_by_user.html.twig new file mode 100644 index 00000000..60599536 --- /dev/null +++ b/templates/reporting/week_by_user.html.twig @@ -0,0 +1,112 @@ +{% extends 'reporting/layout.html.twig' %} + +{% block report_title %}{{ 'report_user_week'|trans({}, 'reporting') }}{% endblock %} + +{% block report %} + + {% set hasData = false %} + {% for day in days %} + {% if day.details is not empty %} + {% set hasData = true %} + {% endif %} + {% endfor %} + + {% embed '@AdminLTE/Widgets/box-widget.html.twig' %} + {% import "macros/widgets.html.twig" as widgets %} + {% block box_before %} + {{ form_start(form, {'action': path('report_user_week'), 'attr': {'class': 'form-inline'}}) }} + {% endblock %} + {% block box_after %} + {{ form_end(form) }} + {% endblock %} + {% block box_title %} + {% if form.user is defined %} + {{ form_widget(form.user) }} + {% else %} + {{ widgets.username(user) }} + {% endif %} + {{ form_widget(form.date) }} + {% endblock %} + {% block box_body_class %}user-week-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %} + {% block box_body %} + {% if not hasData %} + {{ widgets.nothing_found() }} + {% else %} + + + + + {% for day in days %} + + {% endfor %} + + {% for project in rows %} + + + + {% for day in project.days %} + + {% endfor %} + + {% for activity in project.activities %} + + + + {% for day in activity.days %} + + {% endfor %} + + {% endfor %} + {% endfor %} + {% set total = 0 %} + + {% for day in days %} + {% set total = total + day.totalDuration %} + {% endfor %} + + + {% for day in days %} + + {% endfor %} + +
+ {{ day.day|day_name(true) }}
+ {{ day.day|date_format('d.m') }} +
+ {{ widgets.label_project(project.project) }} + {{ project.duration|duration }} + {% if day.duration > 0 %} + {{ day.duration|duration }} + {% endif %} +
+ {{ widgets.label_activity(activity.activity) }} + {{ activity.duration|duration }} + {% if day.duration > 0 %} + {{ day.duration|duration }} + {% endif %} +
{{ total|duration }} + {% if day.totalDuration > 0 %} + {{ day.totalDuration|duration }} + {% endif %} +
+ {% endif %} + {% endblock %} + {% endembed %} + +{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/tests/Controller/ReportingControllerTest.php b/tests/Controller/ReportingControllerTest.php index ed5a11ee..d064744c 100644 --- a/tests/Controller/ReportingControllerTest.php +++ b/tests/Controller/ReportingControllerTest.php @@ -10,6 +10,7 @@ namespace App\Tests\Controller; use App\Entity\User; +use App\Tests\DataFixtures\TimesheetFixtures; /** * @group integration @@ -21,6 +22,16 @@ class ReportingControllerTest extends ControllerBaseTest $this->assertUrlIsSecured('/reporting'); } + public function testWeekByUserIsSecure() + { + $this->assertUrlIsSecured('/reporting/week_by_user'); + } + + public function testMonthByUserIsSecure() + { + $this->assertUrlIsSecured('/reporting/month_by_user'); + } + public function testMonthlyListIsSecure() { $this->assertUrlIsSecured('/reporting/monthly_users_list'); @@ -31,17 +42,53 @@ class ReportingControllerTest extends ControllerBaseTest $this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/monthly_users_list'); } - public function testDefaultUsersMonthReport() + protected function importReportingFixture(string $role) + { + $fixture = new TimesheetFixtures(); + $fixture->setAmount(50); + $fixture->setAmountRunning(10); + $fixture->setUser($this->getUserByRole($role)); + $fixture->setStartDate(new \DateTime()); + $this->importFixture($fixture); + } + + public function testRedirectForDefaultReportUrl() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->assertAccessIsGranted($client, '/reporting/'); + $this->importReportingFixture(User::ROLE_USER); + $this->request($client, '/reporting/'); + $this->assertIsRedirect($client, $this->createUrl('/reporting/week_by_user')); + $client->followRedirect(); + self::assertStringContainsString('
getResponse()->getContent()); + $option = $client->getCrawler()->filterXPath("//select[@id='user']/option[@selected]"); + self::assertEquals(4, $option->attr('value')); + } + + public function testUserMonthReport() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->importReportingFixture(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/reporting/month_by_user?user=4&date=12999119191'); self::assertStringContainsString('
count()); } public function testMonthlyUsersReport() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + $this->importReportingFixture(User::ROLE_TEAMLEAD); $this->assertAccessIsGranted($client, '/reporting/monthly_users_list'); self::assertStringContainsString('
count()); } } diff --git a/tests/Entity/ActivityTest.php b/tests/Entity/ActivityTest.php index 10dbf8fc..288ece92 100644 --- a/tests/Entity/ActivityTest.php +++ b/tests/Entity/ActivityTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Entity; +use App\Constants; use App\Entity\Activity; use App\Entity\ActivityMeta; use App\Entity\Project; @@ -58,6 +59,9 @@ class ActivityTest extends TestCase $this->assertInstanceOf(Activity::class, $sut->setColor('#fffccc')); $this->assertEquals('#fffccc', $sut->getColor()); + $this->assertInstanceOf(Activity::class, $sut->setColor(Constants::DEFAULT_COLOR)); + $this->assertNull($sut->getColor()); + $this->assertInstanceOf(Activity::class, $sut->setBudget(12345.67)); $this->assertEquals(12345.67, $sut->getBudget()); diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php index 559588b6..72a89672 100644 --- a/tests/Entity/CustomerTest.php +++ b/tests/Entity/CustomerTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Entity; +use App\Constants; use App\Entity\Customer; use App\Entity\CustomerMeta; use App\Entity\Team; @@ -72,6 +73,9 @@ class CustomerTest extends TestCase self::assertInstanceOf(Customer::class, $sut->setColor('#fffccc')); self::assertEquals('#fffccc', $sut->getColor()); + self::assertInstanceOf(Customer::class, $sut->setColor(Constants::DEFAULT_COLOR)); + self::assertNull($sut->getColor()); + self::assertInstanceOf(Customer::class, $sut->setCompany('test company')); self::assertEquals('test company', $sut->getCompany()); diff --git a/tests/Entity/ProjectTest.php b/tests/Entity/ProjectTest.php index d1298521..49964554 100644 --- a/tests/Entity/ProjectTest.php +++ b/tests/Entity/ProjectTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Entity; +use App\Constants; use App\Entity\Customer; use App\Entity\Project; use App\Entity\ProjectMeta; @@ -82,6 +83,9 @@ class ProjectTest extends TestCase self::assertInstanceOf(Project::class, $sut->setColor('#fffccc')); self::assertEquals('#fffccc', $sut->getColor()); + self::assertInstanceOf(Project::class, $sut->setColor(Constants::DEFAULT_COLOR)); + self::assertNull($sut->getColor()); + self::assertInstanceOf(Project::class, $sut->setVisible(false)); self::assertFalse($sut->isVisible()); diff --git a/tests/Reporting/AbstractDateByUserTest.php b/tests/Reporting/AbstractDateByUserTest.php new file mode 100644 index 00000000..46cfac3a --- /dev/null +++ b/tests/Reporting/AbstractDateByUserTest.php @@ -0,0 +1,43 @@ +createSut(); + self::assertNull($sut->getDate()); + self::assertNull($sut->getUser()); + } + + public function testSetter() + { + $date = new \DateTime('2019-05-27'); + $user = new User(); + $user->setAlias('sdfsdfdsdf'); + + $sut = $this->createSut(); + self::assertInstanceOf(DateByUser::class, $sut->setDate($date)); + self::assertInstanceOf(DateByUser::class, $sut->setUser($user)); + + self::assertSame($date, $sut->getDate()); + self::assertSame($user, $sut->getUser()); + } +} diff --git a/tests/Reporting/MonthByUserTest.php b/tests/Reporting/MonthByUserTest.php new file mode 100644 index 00000000..651d16e1 --- /dev/null +++ b/tests/Reporting/MonthByUserTest.php @@ -0,0 +1,25 @@ +createDateTimeFactory(self::TEST_TIMEZONE); + $this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName()); + } + + public function testGetTimezoneWithFallbackTimezone() + { + $sut = $this->createDateTimeFactory(); + $this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName()); + } + + public function testGetStartOfMonth() + { + $expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE)); + + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->getStartOfMonth(); + $this->assertEquals(0, $dateTime->format('H')); + $this->assertEquals(0, $dateTime->format('i')); + $this->assertEquals(0, $dateTime->format('s')); + $this->assertEquals(1, $dateTime->format('d')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + } + + public function testGetEndOfMonth() + { + $expected = new DateTime('last day of this month', new DateTimeZone(self::TEST_TIMEZONE)); + + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->getEndOfMonth(); + $this->assertEquals(23, $dateTime->format('H')); + $this->assertEquals(59, $dateTime->format('i')); + $this->assertEquals(59, $dateTime->format('s')); + $this->assertEquals($expected->format('d'), $dateTime->format('d')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + } + + public function testGetStartOfWeek() + { + $expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE)); + + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->getStartOfWeek($expected); + + $this->assertEquals(0, $dateTime->format('H')); + $this->assertEquals(0, $dateTime->format('i')); + $this->assertEquals(0, $dateTime->format('s')); + $this->assertEquals(23, $dateTime->format('d')); + $this->assertEquals(1, $dateTime->format('N')); + $this->assertEquals('Monday', $dateTime->format('l')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + + $expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE)); + $dateTime = $sut->getStartOfWeek(); + + $this->assertEquals(0, $dateTime->format('H')); + $this->assertEquals(0, $dateTime->format('i')); + $this->assertEquals(0, $dateTime->format('s')); + $this->assertEquals(1, $dateTime->format('N')); + $this->assertEquals('Monday', $dateTime->format('l')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + } + + public function testGetEndOfWeek() + { + $expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE)); + + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->getEndOfWeek($expected); + + $this->assertEquals(23, $dateTime->format('H')); + $this->assertEquals(59, $dateTime->format('i')); + $this->assertEquals(59, $dateTime->format('s')); + $this->assertEquals(29, $dateTime->format('d')); + $this->assertEquals(7, $dateTime->format('N')); + $this->assertEquals('Sunday', $dateTime->format('l')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + + $expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE)); + $dateTime = $sut->getEndOfWeek(); + + $this->assertEquals(23, $dateTime->format('H')); + $this->assertEquals(59, $dateTime->format('i')); + $this->assertEquals(59, $dateTime->format('s')); + $this->assertEquals(7, $dateTime->format('N')); + $this->assertEquals('Sunday', $dateTime->format('l')); + $this->assertEquals($expected->format('m'), $dateTime->format('m')); + $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + } + + public function testCreateDateTime() + { + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->createDateTime('2015-07-24 13:45:21'); + $this->assertEquals(13, $dateTime->format('H')); + $this->assertEquals(45, $dateTime->format('i')); + $this->assertEquals(21, $dateTime->format('s')); + $this->assertEquals('24', $dateTime->format('d')); + $this->assertEquals('07', $dateTime->format('m')); + $this->assertEquals('2015', $dateTime->format('Y')); + $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); + } + + public function testCreateDateTimeWithDefaultValue() + { + $expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE)); + + $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $dateTime = $sut->createDateTime(); + $difference = $expected->getTimestamp() - $dateTime->getTimestamp(); + // poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects + $this->assertTrue(2 >= $difference); + } +} diff --git a/tests/Timesheet/UserDateTimeFactoryTest.php b/tests/Timesheet/UserDateTimeFactoryTest.php index ad37f44f..678db403 100644 --- a/tests/Timesheet/UserDateTimeFactoryTest.php +++ b/tests/Timesheet/UserDateTimeFactoryTest.php @@ -14,80 +14,27 @@ use App\Timesheet\UserDateTimeFactory; use PHPUnit\Framework\TestCase; /** + * @covers \App\Timesheet\DateTimeFactory * @covers \App\Timesheet\UserDateTimeFactory */ class UserDateTimeFactoryTest extends TestCase { - public const TEST_TIMEZONE = 'Europe/London'; + public const TEST_TIMEZONE = 'Africa/Asmara'; - protected function createDateTimeFactory(?string $timezone = null): UserDateTimeFactory + protected function createUserDateTimeFactory(?string $timezone = null): UserDateTimeFactory { return (new UserDateTimeFactoryFactory($this))->create($timezone); } public function testGetTimezone() { - $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); + $sut = $this->createUserDateTimeFactory(self::TEST_TIMEZONE); $this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName()); } public function testGetTimezoneWithFallbackTimezone() { - $sut = $this->createDateTimeFactory(); + $sut = $this->createUserDateTimeFactory(); $this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName()); } - - public function testGetStartOfMonth() - { - $expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE)); - - $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); - $dateTime = $sut->getStartOfMonth(); - $this->assertEquals(0, $dateTime->format('H')); - $this->assertEquals(0, $dateTime->format('i')); - $this->assertEquals(0, $dateTime->format('s')); - $this->assertEquals(1, $dateTime->format('d')); - $this->assertEquals($expected->format('m'), $dateTime->format('m')); - $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); - $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); - } - - public function testGetEndOfMonth() - { - $expected = new \DateTime('last day of this month', new \DateTimeZone(self::TEST_TIMEZONE)); - - $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); - $dateTime = $sut->getEndOfMonth(); - $this->assertEquals(23, $dateTime->format('H')); - $this->assertEquals(59, $dateTime->format('i')); - $this->assertEquals(59, $dateTime->format('s')); - $this->assertEquals($expected->format('d'), $dateTime->format('d')); - $this->assertEquals($expected->format('m'), $dateTime->format('m')); - $this->assertEquals($expected->format('Y'), $dateTime->format('Y')); - $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); - } - - public function testCreateDateTime() - { - $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); - $dateTime = $sut->createDateTime('2015-07-24 13:45:21'); - $this->assertEquals(13, $dateTime->format('H')); - $this->assertEquals(45, $dateTime->format('i')); - $this->assertEquals(21, $dateTime->format('s')); - $this->assertEquals('24', $dateTime->format('d')); - $this->assertEquals('07', $dateTime->format('m')); - $this->assertEquals('2015', $dateTime->format('Y')); - $this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName()); - } - - public function testCreateDateTimeWithDefaultValue() - { - $expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE)); - - $sut = $this->createDateTimeFactory(self::TEST_TIMEZONE); - $dateTime = $sut->createDateTime(); - $difference = $expected->getTimestamp() - $dateTime->getTimestamp(); - // poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects - $this->assertTrue(2 >= $difference); - } } diff --git a/tests/Twig/DateExtensionsTest.php b/tests/Twig/DateExtensionsTest.php index f20b81f0..9c1b743f 100644 --- a/tests/Twig/DateExtensionsTest.php +++ b/tests/Twig/DateExtensionsTest.php @@ -139,20 +139,27 @@ class DateExtensionsTest extends TestCase /** * @dataProvider getMonthNameTestData */ - public function testMonthName(string $locale, string $date, string $expectedName) + public function testMonthName(string $locale, string $date, string $expectedName, bool $withYear = false) { $sut = $this->getSut($locale, []); - self::assertEquals($expectedName, $sut->monthName(new \DateTime($date))); + self::assertEquals($expectedName, $sut->monthName(new \DateTime($date), $withYear)); } public function getMonthNameTestData() { return [ - ['de', '2020-07-09 23:59:59', 'Juli'], - ['en', '2020-07-09 23:59:59', 'July'], - ['de', 'January 2016', 'Januar'], - ['en', 'January 2016', 'January'], - ['en', '2016-12-23', 'December'], + ['de', '2020-07-09 23:59:59', 'Juli', false], + ['en', '2020-07-09 23:59:59', 'July', false], + ['de', 'January 2016', 'Januar', false], + ['en', 'January 2016', 'January', false], + ['en', '2016-12-23', 'December', false], + ['ru', '2016-12-23', 'декабрь', false], + ['de', '2020-07-09 23:59:59', 'Juli 2020', true], + ['en', '2020-07-09 23:59:59', 'July 2020', true], + ['de', 'January 2016', 'Januar 2016', true], + ['en', 'January 2016', 'January 2016', true], + ['en', '2015-12-23', 'December 2015', true], + ['ru', '2015-12-23', 'декабрь 2015', true], ]; } diff --git a/tests/Twig/ExtensionsTest.php b/tests/Twig/ExtensionsTest.php index a58bf4cd..bbe8fe51 100644 --- a/tests/Twig/ExtensionsTest.php +++ b/tests/Twig/ExtensionsTest.php @@ -9,6 +9,9 @@ namespace App\Tests\Twig; +use App\Entity\Activity; +use App\Entity\Customer; +use App\Entity\Project; use App\Entity\User; use App\Twig\Extensions; use PHPUnit\Framework\TestCase; @@ -27,7 +30,7 @@ class ExtensionsTest extends TestCase public function testGetFilters() { - $filters = ['docu_link', 'multiline_indent']; + $filters = ['docu_link', 'multiline_indent', 'color']; $sut = $this->getSut(); $twigFilters = $sut->getFilters(); $this->assertCount(\count($filters), $twigFilters); @@ -116,4 +119,39 @@ sdfsdf' . PHP_EOL . "\n" . $sut = $this->getSut(); self::assertEquals(implode("\n", $expected), $sut->multilineIndent($string, $indent)); } + + public function testColor() + { + $sut = $this->getSut(); + + $globalActivity = new Activity(); + self::assertNull($sut->color($globalActivity)); + + $globalActivity->setColor('#000001'); + self::assertEquals('#000001', $sut->color($globalActivity)); + + $customer = new Customer(); + self::assertNull($sut->color($customer)); + + $customer->setColor('#000004'); + self::assertEquals('#000004', $sut->color($customer)); + + $project = new Project(); + self::assertNull($sut->color($project)); + + $project->setCustomer($customer); + self::assertEquals('#000004', $sut->color($project)); + + $project->setColor('#000003'); + self::assertEquals('#000003', $sut->color($project)); + + $activity = new Activity(); + self::assertNull($sut->color($activity)); + + $activity->setProject($project); + self::assertEquals('#000003', $sut->color($activity)); + + $activity->setColor('#000002'); + self::assertEquals('#000002', $sut->color($activity)); + } } diff --git a/tests/Twig/ReportingExtensionTest.php b/tests/Twig/ReportingExtensionTest.php index ee155be7..6216d5fe 100644 --- a/tests/Twig/ReportingExtensionTest.php +++ b/tests/Twig/ReportingExtensionTest.php @@ -57,6 +57,6 @@ class ReportingExtensionTest extends TestCase $sut = $this->getSut(true); $reports = $sut->getAvailableReports(new User()); self::assertIsArray($reports); - self::assertCount(2, $reports); + self::assertCount(3, $reports); } } diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 420cf2f7..49fe1c7d 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -648,6 +648,10 @@ + + stats.workingTimeWeek + Kalenderweek %week% + stats.durationToday Prestaties vandaag diff --git a/translations/reporting.de.xlf b/translations/reporting.de.xlf index f17fd1d2..0d11fa3c 100644 --- a/translations/reporting.de.xlf +++ b/translations/reporting.de.xlf @@ -6,6 +6,10 @@ reporting.title Reporting + + report_user_week + Wochenansicht für einen Benutzer + report_user_month Monatsansicht für einen Benutzer diff --git a/translations/reporting.en.xlf b/translations/reporting.en.xlf index 2bf56be2..af7e1c36 100644 --- a/translations/reporting.en.xlf +++ b/translations/reporting.en.xlf @@ -6,6 +6,10 @@ reporting.title Reporting + + report_user_week + Weekly view for one user + report_user_month Monthly view for one user diff --git a/translations/reporting.nl.xlf b/translations/reporting.nl.xlf new file mode 100644 index 00000000..0178ef23 --- /dev/null +++ b/translations/reporting.nl.xlf @@ -0,0 +1,23 @@ + + + + + + reporting.title + Rapporten + + + report_user_week + Weekoverzicht voor een user + + + report_user_month + Maandoverzicht voor een user + + + report_monthly_users + Maandoverzicht voor alle users + + + + From 9597015413f632a6b1f2b15ccb62d2fc32dc9ec0 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 18 Aug 2020 11:39:41 +0200 Subject: [PATCH 04/12] cleanup global context usage in widgets --- src/Controller/DashboardController.php | 14 ++- src/Repository/WidgetRepository.php | 49 +++------ src/Twig/EventExtensions.php | 60 +---------- src/Twig/MarkdownExtension.php | 2 +- src/Twig/Runtime/ThemeEventExtension.php | 49 +++++++++ src/Widget/Type/AbstractWidgetType.php | 8 +- src/Widget/Type/Counter.php | 7 +- src/Widget/Type/DailyWorkingTimeChart.php | 26 ++--- src/Widget/Type/PaginatedWorkingTimeChart.php | 51 +++++---- src/Widget/Type/SimpleStatisticChart.php | 101 ++++++++++++++++++ src/Widget/Type/UserTeamProjects.php | 15 +-- src/Widget/Type/UserTeams.php | 15 +-- src/Widget/Type/UserWidget.php | 22 ++++ src/Widget/Type/YearChart.php | 2 +- src/Widget/WidgetService.php | 8 +- .../paginatedworkingtimechart.html.twig | 2 +- tests/Controller/WidgetControllerTest.php | 34 ++++++ tests/Repository/WidgetRepositoryTest.php | 20 ++-- tests/Twig/EventExtensionsTest.php | 39 +++++++ .../Twig/Runtime/ThemeEventExtensionTest.php | 48 +++++++++ tests/Twig/WidgetExtensionTest.php | 6 +- .../Renderer/CompoundChartRendererTest.php | 4 +- .../Renderer/CompoundRowRendererTest.php | 4 +- .../Renderer/SimpleWidgetRendererTest.php | 2 - ...AbstractSimpleStatisticsWidgetTypeTest.php | 26 +++++ tests/Widget/Type/AbstractWidgetTypeTest.php | 9 +- tests/Widget/Type/CounterTest.php | 11 +- .../Widget/Type/DailyWorkingTimeChartTest.php | 18 ++-- tests/Widget/Type/YearChartTest.php | 8 +- 29 files changed, 458 insertions(+), 202 deletions(-) create mode 100644 src/Twig/Runtime/ThemeEventExtension.php create mode 100644 src/Widget/Type/SimpleStatisticChart.php create mode 100644 src/Widget/Type/UserWidget.php create mode 100644 tests/Controller/WidgetControllerTest.php create mode 100644 tests/Twig/EventExtensionsTest.php create mode 100644 tests/Twig/Runtime/ThemeEventExtensionTest.php create mode 100644 tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php 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/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/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 9e73af88..b33c6c60 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -11,12 +11,11 @@ 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'; @@ -24,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); @@ -65,16 +63,20 @@ 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())); } $activities = []; 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/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/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/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/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..43ad9d19 100644 --- a/tests/Widget/Type/CounterTest.php +++ b/tests/Widget/Type/CounterTest.php @@ -9,6 +9,7 @@ namespace App\Tests\Widget\Type; +use App\Repository\TimesheetRepository; use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\Counter; use App\Widget\Type\SimpleWidget; @@ -17,11 +18,14 @@ use App\Widget\Type\SimpleWidget; * @covers \App\Widget\Type\Counter * @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 +41,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 6989842d..166e7c62 100644 --- a/tests/Widget/Type/DailyWorkingTimeChartTest.php +++ b/tests/Widget/Type/DailyWorkingTimeChartTest.php @@ -12,8 +12,6 @@ namespace App\Tests\Widget\Type; 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 +28,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() @@ -107,12 +105,8 @@ class DailyWorkingTimeChartTest extends TestCase ]; }); - $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(2, $data); self::assertArrayHasKey('activities', $data); diff --git a/tests/Widget/Type/YearChartTest.php b/tests/Widget/Type/YearChartTest.php index 4b41b865..8c14932e 100644 --- a/tests/Widget/Type/YearChartTest.php +++ b/tests/Widget/Type/YearChartTest.php @@ -9,17 +9,21 @@ namespace App\Tests\Widget\Type; +use App\Repository\TimesheetRepository; use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\YearChart; /** * @covers \App\Widget\Type\YearChart */ -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 From 367a901207068cfffa0170e8500e94e1a2479a5b Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 18 Aug 2020 15:03:46 +0200 Subject: [PATCH 05/12] allow empty strings as time format --- src/Validator/Constraints/TimeFormatValidator.php | 4 ++++ tests/Validator/Constraints/TimeFormatValidatorTest.php | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) 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/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'], From 8d1e24ae903834834d8db4bfcfa2d1c1a0b89fb6 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 18 Aug 2020 15:06:49 +0200 Subject: [PATCH 06/12] removed unused dependency --- src/EventSubscriber/UserPreferenceSubscriber.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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; } From fbb3dd89cf88c1cfb8d356046ce6ddd0315652e7 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 18 Aug 2020 15:08:43 +0200 Subject: [PATCH 07/12] removed unused dependency --- tests/EventSubscriber/UserPreferenceSubscriberTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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); } } From d8ae7635d130d68a37d24c5598c9ddd182665413 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 18 Aug 2020 23:22:16 +0200 Subject: [PATCH 08/12] fix order and update of user settings form --- src/Controller/ProfileController.php | 81 ++++++++++--------- src/Entity/User.php | 2 +- src/Event/UserPreferenceEvent.php | 11 +-- src/EventSubscriber/UserProfileSubscriber.php | 25 ++---- src/Form/Type/UserPreferenceType.php | 6 +- templates/user/form.html.twig | 4 +- tests/Controller/ProfileControllerTest.php | 34 ++++---- 7 files changed, 74 insertions(+), 89 deletions(-) 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/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/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/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')); } } From 6aac85bddb19057be3da0e9fc942037074536430 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 19 Aug 2020 00:03:11 +0200 Subject: [PATCH 09/12] updated test --- tests/Controller/SystemConfigurationControllerTest.php | 2 -- 1 file changed, 2 deletions(-) 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 ); From 15c3acfa0b6f4156ec852a6f1ccc9b290b917a62 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 19 Aug 2020 00:37:49 +0200 Subject: [PATCH 10/12] updated test --- tests/Widget/Type/CounterTest.php | 1 + .../Widget/Type/DailyWorkingTimeChartTest.php | 22 +++++++++++++++++-- tests/Widget/Type/YearChartTest.php | 2 ++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/Widget/Type/CounterTest.php b/tests/Widget/Type/CounterTest.php index 43ad9d19..b887e6f0 100644 --- a/tests/Widget/Type/CounterTest.php +++ b/tests/Widget/Type/CounterTest.php @@ -16,6 +16,7 @@ use App\Widget\Type\SimpleWidget; /** * @covers \App\Widget\Type\Counter + * @covers \App\Widget\Type\SimpleStatisticChart * @covers \App\Widget\Type\SimpleWidget */ class CounterTest extends AbstractSimpleStatisticsWidgetTypeTest diff --git a/tests/Widget/Type/DailyWorkingTimeChartTest.php b/tests/Widget/Type/DailyWorkingTimeChartTest.php index 166e7c62..f24b7630 100644 --- a/tests/Widget/Type/DailyWorkingTimeChartTest.php +++ b/tests/Widget/Type/DailyWorkingTimeChartTest.php @@ -9,6 +9,8 @@ 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; @@ -98,10 +100,18 @@ 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] + ]] ]; }); @@ -112,6 +122,14 @@ class DailyWorkingTimeChartTest extends TestCase 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 8c14932e..03bfd935 100644 --- a/tests/Widget/Type/YearChartTest.php +++ b/tests/Widget/Type/YearChartTest.php @@ -15,6 +15,8 @@ use App\Widget\Type\YearChart; /** * @covers \App\Widget\Type\YearChart + * @covers \App\Widget\Type\SimpleStatisticChart + * @covers \App\Widget\Type\SimpleWidget */ class YearChartTest extends AbstractSimpleStatisticsWidgetTypeTest { From ec612d1046d51ae8ffc15bec6022ad83fca7e045 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 19 Aug 2020 10:08:25 +0200 Subject: [PATCH 11/12] fix duration calculation for multiple entries of same activity --- src/Repository/TimesheetRepository.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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; From 1de76ba266a60294887ad8f39c5d7c1d50fac391 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 19 Aug 2020 10:46:08 +0200 Subject: [PATCH 12/12] added link to reporting documentation --- templates/reporting/actions.html.twig | 1 + 1 file changed, 1 insertion(+) 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 %}