support different formats in user timesheet exports (#1222)

This commit is contained in:
Kevin Papst
2019-11-08 16:10:38 +01:00
committed by GitHub
parent 0705c26513
commit fa1c79e15c
75 changed files with 1872 additions and 951 deletions

View File

@@ -68,7 +68,7 @@ install:
- composer require zendframework/zend-ldap
script:
- if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; fi;
- if [[ $CODECOVERAGE == 1 ]]; then TEST_WITH_BUNDLES=1 vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; fi;
- if [[ $CODECOVERAGE == 0 ]]; then vendor/bin/phpunit tests/; fi;
- if [[ $DB == 'sqlite' ]]; then cp tests/.env.dist.sqlite .env; fi;
- if [[ $DB == "mysql" || $DB == "mariadb" ]]; then cp tests/.env.dist.mysql .env; fi;

View File

@@ -74,9 +74,6 @@ services:
App\Plugin\PluginManager:
arguments: [!tagged kimai.plugin]
App\Export\ServiceExport:
arguments: [!tagged export.renderer]
App\Widget\WidgetService:
arguments:
$renderer: !tagged widget.renderer

View File

@@ -17,6 +17,7 @@ use App\Form\UserPasswordType;
use App\Form\UserPreferencesForm;
use App\Form\UserRolesType;
use App\Form\UserTeamsType;
use App\Repository\TeamRepository;
use App\Repository\TimesheetRepository;
use App\Voter\UserVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
@@ -38,21 +39,21 @@ class ProfileController extends AbstractController
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
private $dispatcher;
/**
* @var UserPasswordEncoderInterface
*/
protected $encoder;
private $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
* @param EventDispatcherInterface $dispatcher
* @var TeamRepository
*/
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
private $teams;
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher, TeamRepository $teams)
{
$this->encoder = $encoder;
$this->dispatcher = $dispatcher;
$this->teams = $teams;
}
/**
@@ -283,7 +284,7 @@ class ProfileController extends AbstractController
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
$forms['api-token'] = $apiTokenForm->createView();
}
if ($this->isGranted(UserVoter::TEAMS, $user)) {
if ($this->isGranted(UserVoter::TEAMS, $user) && $this->teams->count([]) > 0) {
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
$forms['teams'] = $teamsForm->createView();
}

View File

@@ -15,6 +15,7 @@ use App\Entity\Tag;
use App\Entity\Timesheet;
use App\Event\TimesheetMetaDefinitionEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\ServiceExport;
use App\Form\TimesheetEditForm;
use App\Form\Toolbar\TimesheetToolbarForm;
use App\Repository\ActivityRepository;
@@ -53,19 +54,25 @@ abstract class TimesheetAbstractController extends AbstractController
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var ServiceExport
*/
protected $exportService;
public function __construct(
UserDateTimeFactory $dateTime,
TimesheetConfiguration $configuration,
TimesheetRepository $repository,
TrackingModeService $service,
EventDispatcherInterface $dispatcher
EventDispatcherInterface $dispatcher,
ServiceExport $exportService
) {
$this->dateTime = $dateTime;
$this->configuration = $configuration;
$this->repository = $repository;
$this->trackingModeService = $service;
$this->dispatcher = $dispatcher;
$this->exportService = $exportService;
}
protected function getTrackingMode(): TrackingModeInterface
@@ -227,7 +234,7 @@ abstract class TimesheetAbstractController extends AbstractController
]);
}
protected function export(Request $request, string $renderTemplate, string $location): Response
protected function export(Request $request, string $exporterId): Response
{
$query = new TimesheetQuery();
@@ -252,11 +259,13 @@ abstract class TimesheetAbstractController extends AbstractController
$entries = $this->getRepository()->getTimesheetsForQuery($query);
return $this->render($renderTemplate, [
'entries' => $entries,
'query' => $query,
'metaColumns' => $this->findMetaColumns($query, $location),
]);
$exporter = $this->exportService->getTimesheetExporterById($exporterId);
if (null === $exporter) {
throw $this->createNotFoundException('Invalid timesheet exporter given');
}
return $exporter->render($entries, $query);
}
protected function prepareQuery(TimesheetQuery $query)

View File

@@ -16,6 +16,7 @@ use App\Repository\ProjectRepository;
use App\Repository\TagRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
@@ -31,7 +32,7 @@ class TimesheetController extends TimesheetAbstractController
*
* @param int $page
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
* @return Response
*/
public function indexAction($page, Request $request)
{
@@ -39,15 +40,16 @@ class TimesheetController extends TimesheetAbstractController
}
/**
* @Route(path="/export", name="timesheet_export", methods={"GET"})
* @Route(path="/export/{exporter}", name="timesheet_export", methods={"GET"})
* @Security("is_granted('export_own_timesheet')")
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
* @param string $exporter
* @return Response
*/
public function exportAction(Request $request)
public function exportAction(Request $request, string $exporter)
{
return $this->export($request, 'timesheet/export.html.twig', TimesheetMetaDisplayEvent::TIMESHEET_EXPORT);
return $this->export($request, $exporter);
}
/**
@@ -80,7 +82,7 @@ class TimesheetController extends TimesheetAbstractController
/**
* Used for the initial page rendering.
*
* @return \Symfony\Component\HttpFoundation\Response
* @return Response
*/
public function activeEntriesAction()
{

View File

@@ -18,6 +18,7 @@ use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
@@ -33,7 +34,7 @@ class TimesheetTeamController extends TimesheetAbstractController
*
* @param int $page
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
* @return Response
*/
public function indexAction($page, Request $request)
{
@@ -41,14 +42,15 @@ class TimesheetTeamController extends TimesheetAbstractController
}
/**
* @Route(path="/export", name="admin_timesheet_export", methods={"GET"})
* @Route(path="/export/{exporter}", name="admin_timesheet_export", methods={"GET"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
* @param string $exporter
* @return Response
*/
public function exportAction(Request $request)
public function exportAction(Request $request, string $exporter)
{
return $this->export($request, 'timesheet-team/export.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET_EXPORT);
return $this->export($request, $exporter);
}
/**

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\DependencyInjection\Compiler;
use App\Export\ServiceExport;
use App\Kernel;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Dynamically adds all dependencies to the ExportService.
*/
class ExportServiceCompilerPass implements CompilerPassInterface
{
/**
* @param ContainerBuilder $container
* @throws \Exception
*/
public function process(ContainerBuilder $container)
{
// always first check if the primary service is defined
if (!$container->has(ServiceExport::class)) {
return;
}
$definition = $container->findDefinition(ServiceExport::class);
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('addRenderer', [new Reference($id)]);
}
$taggedExporter = $container->findTaggedServiceIds(Kernel::TAG_TIMESHEET_EXPORTER);
foreach ($taggedExporter as $id => $tags) {
$definition->addMethodCall('addTimesheetExporter', [new Reference($id)]);
}
}
}

View File

@@ -0,0 +1,407 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Timesheet;
use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Event\MetaDisplayEventInterface;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Event\UserPreferenceDisplayEvent;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use DateTime;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*/
abstract class AbstractSpreadsheetRenderer
{
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
public const TIME_FORMAT = 'hh:mm';
public const DURATION_FORMAT = '[hh]:mm';
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
/**
* @var DateExtensions
*/
protected $dateExtension;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var AuthorizationCheckerInterface
*/
protected $voter;
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter)
{
$this->translator = $translator;
$this->dateExtension = $dateExtension;
$this->dispatcher = $dispatcher;
$this->voter = $voter;
}
protected function isRenderRate(TimesheetQuery $query): bool
{
return true;
}
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
}
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
}
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
}
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
$style = $sheet->getStyleByColumnAndRow($column, $row);
$style->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
}
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
{
if (null === $duration) {
$duration = 0;
}
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
}
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
}
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency)
{
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
sprintf(self::RATE_FORMAT_LEFT, $currency)
);
}
/**
* @param MetaDisplayEventInterface $event
* @return MetaTableTypeInterface[]
*/
protected function findMetaColumns(MetaDisplayEventInterface $event): array
{
$this->dispatcher->dispatch($event);
return $event->getFields();
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Spreadsheet
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
{
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::EXPORT));
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
$this->dispatcher->dispatch($event);
$userPreferences = $event->getPreferences();
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$showRates = $this->isRenderRate($query);
$recordsHeaderColumn = 1;
$recordsHeaderRow = 1;
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
if ($showRates) {
$sheet->setCellValueByColumnAndRow(
$recordsHeaderColumn++,
$recordsHeaderRow,
$this->translator->trans('label.rate')
);
}
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
if ($showRates) {
$sheet->setCellValueByColumnAndRow(
$recordsHeaderColumn++,
$recordsHeaderRow,
$this->translator->trans('label.hourlyRate')
);
$sheet->setCellValueByColumnAndRow(
$recordsHeaderColumn++,
$recordsHeaderRow,
$this->translator->trans('label.fixedRate')
);
}
foreach ($timesheetMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($customerMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($projectMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($activityMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($userPreferences as $preference) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($preference->getLabel()));
}
$entryHeaderRow = $recordsHeaderRow + 1;
$durationColumn = null;
$rateColumn = null;
foreach ($timesheets as $timesheet) {
$entryHeaderColumn = 1;
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
$entryHeaderColumn++;
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
$entryHeaderColumn++;
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getEnd());
$entryHeaderColumn++;
$this->setDuration($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getDuration());
$durationColumn = $entryHeaderColumn;
$entryHeaderColumn++;
if ($showRates) {
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency);
$rateColumn = $entryHeaderColumn;
$entryHeaderColumn++;
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getUser()->getDisplayName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getActivity()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getDescription());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->translator->trans($exported));
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
$entryHeaderColumn++;
if ($showRates) {
$this->setRate(
$sheet,
$entryHeaderColumn,
$entryHeaderRow,
$timesheet->getHourlyRate(),
$customerCurrency
);
$entryHeaderColumn++;
$this->setRate(
$sheet,
$entryHeaderColumn,
$entryHeaderRow,
$timesheet->getFixedRate(),
$customerCurrency
);
$entryHeaderColumn++;
}
foreach ($timesheetMetaFields as $metaField) {
$metaField = $timesheet->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($customerMetaFields as $metaField) {
$metaField = $timesheet->getProject()->getCustomer()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($projectMetaFields as $metaField) {
$metaField = $timesheet->getProject()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($activityMetaFields as $metaField) {
$metaField = $timesheet->getActivity()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($userPreferences as $preference) {
$metaField = $timesheet->getUser()->getPreference($preference->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
$entryHeaderRow++;
}
if (null !== $durationColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
if (null !== $rateColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
return $spreadsheet;
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$spreadsheet = $this->fromArrayToSpreadsheet($timesheets, $query);
$filename = $this->saveSpreadsheet($spreadsheet);
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
}
/**
* @return string
*/
abstract public function getFileExtension(): string;
/**
* @param mixed $file
* @param string $filename
* @return BinaryFileResponse
*/
protected function getFileResponse($file, $filename): Response
{
$response = new BinaryFileResponse($file);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
$response->headers->set('Content-Type', $this->getContentType());
$response->headers->set('Content-Disposition', $disposition);
$response->deleteFileAfterSend(true);
return $response;
}
/**
* @return string
*/
abstract protected function getContentType(): string;
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class CsvRenderer extends AbstractSpreadsheetRenderer
{
/**
* @return string
*/
public function getFileExtension(): string
{
return '.csv';
}
/**
* @return string
*/
protected function getContentType(): string
{
return 'text/csv';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-csv');
if (false === $filename) {
throw new \Exception('Could not open temporary file');
}
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
$writer->save($filename);
return $filename;
}
public function getId(): string
{
return 'csv';
}
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
}
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Timesheet;
use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Event\MetaDisplayEventInterface;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Event\UserPreferenceDisplayEvent;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
class HtmlRenderer
{
use RendererTrait;
/**
* @var Environment
*/
protected $twig;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher)
{
$this->twig = $twig;
$this->dispatcher = $dispatcher;
}
/**
* @param MetaDisplayEventInterface $event
* @return MetaTableTypeInterface[]
*/
protected function findMetaColumns(MetaDisplayEventInterface $event): array
{
$this->dispatcher->dispatch($event);
return $event->getFields();
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::EXPORT));
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
$this->dispatcher->dispatch($event);
$userPreferences = $event->getPreferences();
$content = $this->twig->render('export/renderer/default.html.twig', [
'entries' => $timesheets,
'query' => $query,
'summaries' => $this->calculateSummary($timesheets),
'timesheetMetaFields' => $timesheetMetaFields,
'customerMetaFields' => $customerMetaFields,
'projectMetaFields' => $projectMetaFields,
'activityMetaFields' => $activityMetaFields,
'userPreferences' => $userPreferences,
]);
$response = new Response();
$response->setContent($content);
return $response;
}
public function getId(): string
{
return 'html';
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\HtmlToPdfConverter;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Twig\Environment;
class PDFRenderer
{
use RendererTrait;
/**
* @var Environment
*/
private $twig;
/**
* @var UserDateTimeFactory
*/
private $dateTime;
/**
* @var HtmlToPdfConverter
*/
private $converter;
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
{
$this->twig = $twig;
$this->dateTime = $dateTime;
$this->converter = $converter;
}
protected function getTemplate(): string
{
return 'export/renderer/pdf.html.twig';
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$content = $this->twig->render($this->getTemplate(), [
'entries' => $timesheets,
'query' => $query,
'now' => $this->dateTime->createDateTime(),
'summaries' => $this->calculateSummary($timesheets),
]);
$content = $this->converter->convertToPdf($content);
$response = new Response($content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'kimai-export.pdf');
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
public function getId(): string
{
return 'pdf';
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use App\Entity\Timesheet;
trait RendererTrait
{
/**
* @param Timesheet[] $timesheets
* @return array
*/
protected function calculateSummary(array $timesheets)
{
$summary = [];
foreach ($timesheets as $timesheet) {
$id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId();
$activityId = $timesheet->getActivity()->getId();
if (!isset($summary[$id])) {
$summary[$id] = [
'customer' => $timesheet->getProject()->getCustomer()->getName(),
'project' => $timesheet->getProject()->getName(),
'activities' => [],
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
'rate' => 0,
'duration' => 0,
];
}
if (!isset($summary[$id]['activities'][$activityId])) {
$summary[$id]['activities'][$activityId] = [
'activity' => $timesheet->getActivity()->getName(),
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
'rate' => 0,
'duration' => 0,
];
}
$duration = $timesheet->getDuration();
if (null === $duration) {
$duration = 0;
}
$summary[$id]['rate'] += $timesheet->getRate();
$summary[$id]['duration'] += $duration;
$summary[$id]['activities'][$activityId]['rate'] += $timesheet->getRate();
$summary[$id]['activities'][$activityId]['duration'] += $duration;
}
asort($summary);
return $summary;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Base;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class XlsxRenderer extends AbstractSpreadsheetRenderer
{
public function getFileExtension(): string
{
return '.xlsx';
}
/**
* @return string
*/
protected function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
if (false === $filename) {
throw new \Exception('Could not open temporary file');
}
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save($filename);
return $filename;
}
public function getId(): string
{
return 'xlsx';
}
}

View File

@@ -9,361 +9,11 @@
namespace App\Export\Renderer;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Timesheet;
use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Event\MetaDisplayEventInterface;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Event\UserPreferenceDisplayEvent;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use DateTime;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Export\Base\AbstractSpreadsheetRenderer as BaseAbstractSpreadsheetRenderer;
abstract class AbstractSpreadsheetRenderer
/**
* @deprecated since 1.6, will be removed with 2.0
*/
abstract class AbstractSpreadsheetRenderer extends BaseAbstractSpreadsheetRenderer
{
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
public const TIME_FORMAT = 'hh:mm';
public const DURATION_FORMAT = '[hh]:mm';
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
/**
* @var DateExtensions
*/
protected $dateExtension;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, EventDispatcherInterface $dispatcher)
{
$this->translator = $translator;
$this->dateExtension = $dateExtension;
$this->dispatcher = $dispatcher;
}
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
}
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
}
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date)
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
}
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
$style = $sheet->getStyleByColumnAndRow($column, $row);
$style->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
}
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
{
if (null === $duration) {
$duration = 0;
}
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
}
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
}
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency)
{
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
sprintf(self::RATE_FORMAT_LEFT, $currency)
);
}
/**
* @param MetaDisplayEventInterface $event
* @return MetaTableTypeInterface[]
*/
protected function findMetaColumns(MetaDisplayEventInterface $event): array
{
$this->dispatcher->dispatch($event);
return $event->getFields();
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Spreadsheet
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
{
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::EXPORT));
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
$this->dispatcher->dispatch($event);
$userPreferences = $event->getPreferences();
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$recordsHeaderColumn = 1;
$recordsHeaderRow = 1;
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
foreach ($timesheetMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($customerMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($projectMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($activityMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaField->getLabel()));
}
foreach ($userPreferences as $preference) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($preference->getLabel()));
}
$entryHeaderRow = $recordsHeaderRow + 1;
$durationColumn = null;
$rateColumn = null;
foreach ($timesheets as $timesheet) {
$entryHeaderColumn = 1;
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
$entryHeaderColumn++;
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
$entryHeaderColumn++;
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getEnd());
$entryHeaderColumn++;
$this->setDuration($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getDuration());
$durationColumn = $entryHeaderColumn;
$entryHeaderColumn++;
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency);
$rateColumn = $entryHeaderColumn;
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getUsername($timesheet));
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getActivity()->getName());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getDescription());
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->translator->trans($exported));
$entryHeaderColumn++;
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
$entryHeaderColumn++;
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getHourlyRate(), $customerCurrency);
$entryHeaderColumn++;
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getFixedRate(), $customerCurrency);
$entryHeaderColumn++;
foreach ($timesheetMetaFields as $metaField) {
$metaField = $timesheet->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($customerMetaFields as $metaField) {
$metaField = $timesheet->getProject()->getCustomer()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($projectMetaFields as $metaField) {
$metaField = $timesheet->getProject()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($activityMetaFields as $metaField) {
$metaField = $timesheet->getActivity()->getMetaField($metaField->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
foreach ($userPreferences as $preference) {
$metaField = $timesheet->getUser()->getPreference($preference->getName());
$metaFieldValue = '';
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
}
$entryHeaderRow++;
}
if (null !== $durationColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
if (null !== $rateColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
return $spreadsheet;
}
protected function getUsername(Timesheet $timesheet): string
{
if (!empty($timesheet->getUser()->getAlias())) {
return $timesheet->getUser()->getAlias();
}
return $timesheet->getUser()->getUsername();
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$spreadsheet = $this->fromArrayToSpreadsheet($timesheets, $query);
$filename = $this->saveSpreadsheet($spreadsheet);
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
}
/**
* @return string
*/
abstract public function getFileExtension(): string;
/**
* @param mixed $file
* @param string $filename
* @return BinaryFileResponse
*/
protected function getFileResponse($file, $filename): Response
{
$response = new BinaryFileResponse($file);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
$response->headers->set('Content-Type', $this->getContentType());
$response->headers->set('Content-Disposition', $disposition);
$response->deleteFileAfterSend(true);
return $response;
}
/**
* @return string
*/
abstract protected function getContentType(): string;
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
}

View File

@@ -9,73 +9,18 @@
namespace App\Export\Renderer;
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
use App\Export\RendererInterface;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
final class CsvRenderer extends BaseCsvRenderer implements RendererInterface
{
/**
* @return string
*/
public function getFileExtension(): string
{
return '.csv';
}
/**
* @return string
*/
protected function getContentType(): string
{
return 'text/csv';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-csv');
if (false === $filename) {
throw new \Exception('Could not open temporary file');
}
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
$writer->save($filename);
return $filename;
}
/**
* @return string
*/
public function getId(): string
{
return 'csv';
}
/**
* @return string
*/
public function getIcon(): string
{
return 'csv';
}
/**
* @return string
*/
public function getTitle(): string
{
return 'csv';
}
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
}
}

View File

@@ -9,105 +9,16 @@
namespace App\Export\Renderer;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Timesheet;
use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Event\MetaDisplayEventInterface;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Event\UserPreferenceDisplayEvent;
use App\Export\Base\HtmlRenderer as BaseHtmlRenderer;
use App\Export\RendererInterface;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
final class HtmlRenderer implements RendererInterface
final class HtmlRenderer extends BaseHtmlRenderer implements RendererInterface
{
use RendererTrait;
/**
* @var Environment
*/
protected $twig;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher)
{
$this->twig = $twig;
$this->dispatcher = $dispatcher;
}
/**
* @param MetaDisplayEventInterface $event
* @return MetaTableTypeInterface[]
*/
protected function findMetaColumns(MetaDisplayEventInterface $event): array
{
$this->dispatcher->dispatch($event);
return $event->getFields();
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::EXPORT));
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
$this->dispatcher->dispatch($event);
$userPreferences = $event->getPreferences();
$content = $this->twig->render('export/renderer/default.html.twig', [
'entries' => $timesheets,
'query' => $query,
'summaries' => $this->calculateSummary($timesheets),
'timesheetMetaFields' => $timesheetMetaFields,
'customerMetaFields' => $customerMetaFields,
'projectMetaFields' => $projectMetaFields,
'activityMetaFields' => $activityMetaFields,
'userPreferences' => $userPreferences,
]);
$response = new Response();
$response->setContent($content);
return $response;
}
/**
* @return string
*/
public function getId(): string
{
return 'html';
}
/**
* @return string
*/
public function getIcon(): string
{
return 'print';
}
/**
* @return string
*/
public function getTitle(): string
{
return 'print';

View File

@@ -9,92 +9,16 @@
namespace App\Export\Renderer;
use App\Entity\Timesheet;
use App\Export\Base\PDFRenderer as BasePDFRenderer;
use App\Export\RendererInterface;
use App\Repository\Query\TimesheetQuery;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\HtmlToPdfConverter;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Twig\Environment;
final class PDFRenderer implements RendererInterface
final class PDFRenderer extends BasePDFRenderer implements RendererInterface
{
use RendererTrait;
/**
* @var Environment
*/
protected $twig;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @var HtmlToPdfConverter
*/
protected $converter;
/**
* @param Environment $twig
* @param UserDateTimeFactory $dateTime
* @param HtmlToPdfConverter $converter
*/
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
{
$this->twig = $twig;
$this->dateTime = $dateTime;
$this->converter = $converter;
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$content = $this->twig->render('export/renderer/pdf.html.twig', [
'entries' => $timesheets,
'query' => $query,
'now' => $this->dateTime->createDateTime(),
'summaries' => $this->calculateSummary($timesheets),
]);
$content = $this->converter->convertToPdf($content);
$response = new Response($content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'kimai-export.pdf');
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
/**
* @return string
*/
public function getId(): string
{
return 'pdf';
}
/**
* @return string
*/
public function getIcon(): string
{
return 'pdf';
}
/**
* @return string
*/
public function getTitle(): string
{
return 'pdf';

View File

@@ -9,55 +9,12 @@
namespace App\Export\Renderer;
use App\Entity\Timesheet;
use App\Export\Base\RendererTrait as BaseRendererTrait;
/**
* @deprecated since 1.6, will be removed with 2.0
*/
trait RendererTrait
{
/**
* @param Timesheet[] $timesheets
* @return array
*/
protected function calculateSummary(array $timesheets)
{
$summary = [];
foreach ($timesheets as $timesheet) {
$id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId();
$activityId = $timesheet->getActivity()->getId();
if (!isset($summary[$id])) {
$summary[$id] = [
'customer' => $timesheet->getProject()->getCustomer()->getName(),
'project' => $timesheet->getProject()->getName(),
'activities' => [],
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
'rate' => 0,
'duration' => 0,
];
}
if (!isset($summary[$id]['activities'][$activityId])) {
$summary[$id]['activities'][$activityId] = [
'activity' => $timesheet->getActivity()->getName(),
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
'rate' => 0,
'duration' => 0,
];
}
$duration = $timesheet->getDuration();
if (null === $duration) {
$duration = 0;
}
$summary[$id]['rate'] += $timesheet->getRate();
$summary[$id]['duration'] += $duration;
$summary[$id]['activities'][$activityId]['rate'] += $timesheet->getRate();
$summary[$id]['activities'][$activityId]['duration'] += $duration;
}
asort($summary);
return $summary;
}
use BaseRendererTrait;
}

View File

@@ -9,65 +9,16 @@
namespace App\Export\Renderer;
use App\Export\Base\XlsxRenderer as BaseXlsxRenderer;
use App\Export\RendererInterface;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
final class XlsxRenderer extends BaseXlsxRenderer implements RendererInterface
{
/**
* @return string
*/
public function getFileExtension(): string
{
return '.xlsx';
}
/**
* @return string
*/
protected function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
/**
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
if (false === $filename) {
throw new \Exception('Could not open temporary file');
}
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save($filename);
return $filename;
}
/**
* @return string
*/
public function getId(): string
{
return 'xlsx';
}
/**
* @return string
*/
public function getIcon(): string
{
return 'xlsx';
}
/**
* @return string
*/
public function getTitle(): string
{
return 'xlsx';

View File

@@ -9,22 +9,17 @@
namespace App\Export;
class ServiceExport
final class ServiceExport
{
/**
* @var RendererInterface[]
*/
protected $renderer = [];
private $renderer = [];
/**
* @param RendererInterface[] $renderer
* @var TimesheetExportInterface[]
*/
public function __construct(iterable $renderer)
{
foreach ($renderer as $render) {
$this->addRenderer($render);
}
}
private $exporter = [];
public function addRenderer(RendererInterface $renderer): ServiceExport
{
@@ -51,4 +46,30 @@ class ServiceExport
return null;
}
public function addTimesheetExporter(TimesheetExportInterface $exporter): ServiceExport
{
$this->exporter[] = $exporter;
return $this;
}
/**
* @return TimesheetExportInterface[]
*/
public function getTimesheetExporter(): array
{
return $this->exporter;
}
public function getTimesheetExporterById(string $id): ?TimesheetExportInterface
{
foreach ($this->exporter as $exporter) {
if ($exporter->getId() === $id) {
return $exporter;
}
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Timesheet;
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
final class CsvRenderer extends BaseCsvRenderer implements TimesheetExportInterface
{
protected function isRenderRate(TimesheetQuery $query): bool
{
if (null !== $query->getUser()) {
return $this->voter->isGranted('view_rate_own_timesheet');
}
return true;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Timesheet;
use App\Entity\Timesheet;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
final class HtmlRenderer implements TimesheetExportInterface
{
/**
* @var Environment
*/
private $twig;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher)
{
$this->twig = $twig;
$this->dispatcher = $dispatcher;
}
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(array $timesheets, TimesheetQuery $query): Response
{
$metaLocation = TimesheetMetaDisplayEvent::TEAM_TIMESHEET_EXPORT;
if (null !== $query->getUser()) {
$metaLocation = TimesheetMetaDisplayEvent::EXPORT;
}
$event = new TimesheetMetaDisplayEvent($query, $metaLocation);
$this->dispatcher->dispatch($event);
$timesheetMetaFields = $event->getFields();
$content = $this->twig->render('timesheet/export.html.twig', [
'entries' => $timesheets,
'query' => $query,
'metaColumns' => $timesheetMetaFields,
]);
$response = new Response();
$response->setContent($content);
return $response;
}
/**
* @return string
*/
public function getId(): string
{
return 'print';
}
}

View File

@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Timesheet;
use App\Export\Base\PDFRenderer as BasePDFRenderer;
use App\Export\TimesheetExportInterface;
final class PDFRenderer extends BasePDFRenderer implements TimesheetExportInterface
{
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export\Timesheet;
use App\Export\Base\XlsxRenderer as BaseXlsxRenderer;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
final class XlsxRenderer extends BaseXlsxRenderer implements TimesheetExportInterface
{
protected function isRenderRate(TimesheetQuery $query): bool
{
return false;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Export;
use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\HttpFoundation\Response;
interface TimesheetExportInterface
{
/**
* @param Timesheet[] $timesheets
* @param TimesheetQuery $query
* @return Response
*/
public function render(array $timesheets, TimesheetQuery $query): Response;
/**
* @return string
*/
public function getId(): string;
}

View File

@@ -11,10 +11,12 @@ namespace App;
use App\DependencyInjection\AppExtension;
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
use App\DependencyInjection\Compiler\WidgetCompilerPass;
use App\Export\RendererInterface as ExportRendererInterface;
use App\Export\TimesheetExportInterface;
use App\Invoice\CalculatorInterface as InvoiceCalculator;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGeneratorInterface;
@@ -49,6 +51,7 @@ class Kernel extends BaseKernel
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
public const TAG_INVOICE_REPOSITORY = 'invoice.repository';
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter';
public function getCacheDir()
{
@@ -71,6 +74,7 @@ class Kernel extends BaseKernel
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
$container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER);
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);
$container->registerForAutoconfiguration(TimesheetExportInterface::class)->addTag(self::TAG_TIMESHEET_EXPORTER);
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
@@ -91,6 +95,10 @@ class Kernel extends BaseKernel
return;
}
if ($this->environment === 'test' && getenv('TEST_WITH_BUNDLES') === false) {
return;
}
$finder = new Finder();
$finder->ignoreUnreadableDirs()->directories()->name('*Bundle');
/** @var SplFileInfo $bundleDir */
@@ -149,6 +157,7 @@ class Kernel extends BaseKernel
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
$container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
$container->addCompilerPass(new WidgetCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
}
@@ -172,6 +181,10 @@ class Kernel extends BaseKernel
// load application routes
$routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob');
if ($this->environment === 'test' && getenv('TEST_WITH_BUNDLES') === false) {
return;
}
// load plugin routes
$pluginsDir = $this->getProjectDir() . '/var/plugins';
if (!file_exists($pluginsDir)) {

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Export\ServiceExport;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class TimesheetExtension extends AbstractExtension
{
/**
* @var ServiceExport
*/
private $service;
public function __construct(ServiceExport $service)
{
$this->service = $service;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('timesheet_exporter', [$this, 'getTimesheetExporter'], []),
];
}
public function getTimesheetExporter(): array
{
$ids = [];
foreach ($this->service->getTimesheetExporter() as $exporter) {
$ids[] = $exporter->getId();
}
return $ids;
}
}

View File

@@ -1,3 +1,11 @@
{% set showUserColumn = true %}
{% set showRateColumn = true %}
{% if query.user %}
{# this is only triggered, if a user exports from his personal timesheet screen#}
{% set showUserColumn = false %}
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
{# TODO if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates, maybe merge view_rate_own_timesheet and view_rate_other_timesheet into a new view_rate permission? #}
{% endif %}
<html>
<head>
<style>
@@ -48,9 +56,17 @@
<tr>
<td align="left">
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
{% if not showUserColumn %}
&ndash;
{{ 'label.user'|trans }}: {{ query.user.displayName }}
{% endif %}
</td>
<td align="right">
{{ 'export.date_copyright'|trans({'%date%': now|date_short ~ ' ' ~ now|date('H:i'), '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{% if kimai_context.branding.company is not empty %}
{{ kimai_context.branding.company }} &ndash; {{ now|date_full }}
{% else %}
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{% endif %}
</td>
</tr>
</table>
@@ -62,6 +78,7 @@ mpdf-->
{{ 'export.period'|trans }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
</p>
<h3>{{ 'export.summary'|trans }}</h3>
<table class="items" width="100%" style="font-size: 9pt; border-collapse: collapse; " cellpadding="8">
<thead>
@@ -69,7 +86,9 @@ mpdf-->
<td>{{ 'label.customer'|trans }}</td>
<td>{{ 'label.project'|trans }}</td>
<td>{{ 'label.duration'|trans }}</td>
{% if showRateColumn %}
<td>{{ 'label.rate'|trans }}</td>
{% endif %}
</tr>
</thead>
<tbody>
@@ -87,7 +106,9 @@ mpdf-->
<tr class="summary">
<td colspan="2"></td>
<td class="totals duration">{{ customerDuration|duration }}</td>
{% if showRateColumn %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% set customerCurrency = summary.currency %}
{% set customer = summary.customer %}
@@ -99,7 +120,9 @@ mpdf-->
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
<td class="duration">{{ summary.duration|duration }}</td>
{% if showRateColumn %}
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
{% endif %}
</tr>
{% set customerDuration = customerDuration + summary.duration %}
{% set customerRate = customerRate + summary.rate %}
@@ -109,7 +132,9 @@ mpdf-->
<tr class="summary">
<td colspan="2"></td>
<td class="totals duration">{{ customerDuration|duration }}</td>
{% if showRateColumn %}
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
{% endif %}
</tr>
{% endif %}
</tbody>
@@ -126,15 +151,19 @@ mpdf-->
<thead>
<tr>
<td>{{ 'label.date'|trans }}</td>
{% if showUserColumn %}
<td>{{ 'label.user'|trans }}</td>
{% endif %}
<td width="">{{ 'label.description'|trans }}</td>
<td>{{ 'label.duration'|trans }}</td>
{% if showRateColumn %}
<td>{{ 'label.rate'|trans }}</td>
{% endif %}
</tr>
</thead>
<tbody>
{% for entry in entries %}
{% set duration = duration + entry.duration %}
{% set rate = rate + entry.rate %}
{% if currency is same as(false) %}
{% set currency = entry.project.customer.currency %}
{% endif %}
@@ -149,6 +178,9 @@ mpdf-->
{{ entry.end|date_time }}
{% endif %}
</td>
{% if showUserColumn %}
<td>{{ entry.user.displayName }}</td>
{% endif %}
<td>
{{ entry.project.customer.name }} - {{ entry.project.name }} - {{ entry.activity.name }}
{% if entry.description is not empty %}
@@ -157,13 +189,28 @@ mpdf-->
{% endif %}
</td>
<td class="duration">{{ entry.duration|duration }}</td>
<td class="cost">{{ entry.rate|money(entry.project.customer.currency) }}</td>
{% if showRateColumn %}
<td class="cost">
{% if is_granted('view_rate', entry) %}
{% set rate = rate + entry.rate %}
{{ entry.rate|money(entry.project.customer.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
<tr class="summary">
{% if showUserColumn %}
<td colspan="3"></td>
{% else %}
<td colspan="2"></td>
{% endif %}
<td class="totals duration">{{ duration|duration }}</td>
{% if showRateColumn %}
<td class="totals cost">{{ rate|money(currency) }}</td>
{% endif %}
</tr>
</tbody>
</table>

View File

@@ -301,7 +301,22 @@
<div class="btn-group">
{%- apply spaceless -%}
{%- for icon,values in actions %}
{{ macro.action_button(icon, values, type) }}
{% if values.children is defined %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ macro.icon(icon) }}&nbsp;
<span class="caret"></span>
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for childIcon,childValues in values.children %}
<li>{{ macro.action_button(childIcon, childValues, false) }}</li>
{% endfor %}
</ul>
</div>
{% else %}
{{ macro.action_button(icon, values, type) }}
{% endif %}
{% endfor -%}
{% endapply %}
</div>

View File

@@ -3,7 +3,16 @@
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}} %}
{% if is_granted('export_own_timesheet') %}
{% set actions = actions|merge({'download': {'url': path('admin_timesheet_export'), 'class': 'toolbar-action'}}) %}
{% set exporterIds = timesheet_exporter() %}
{% if exporterIds|length == 1 %}
{% set actions = actions|merge({'download': {'url': path('admin_timesheet_export', {'exporter': exporterIds.0}), 'class': 'toolbar-action'}}) %}
{% elseif exporterIds|length > 1 %}
{% set children = {} %}
{% for exporter in exporterIds %}
{% set children = children|merge({(exporter): {'title': ('button.' ~ exporter)|trans, 'url': path('admin_timesheet_export', {'exporter': exporter}), 'class': 'toolbar-action exporter-' ~ exporter}}) %}
{% endfor %}
{% set actions = actions|merge({'download': {'children': children}}) %}
{% endif %}
{% endif %}
{% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
{% if is_granted('create_other_timesheet') %}

View File

@@ -1,91 +0,0 @@
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% extends 'invoice/layout.html.twig' %}
{% block invoice %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">
{% if query.begin is not empty and query.end is not empty %}
{% if query.begin|date('m') != query.end|date('m') or query.begin|date('Y') != query.end|date('Y') %}
{{ query.begin|date_short }} - {{ query.end|date_short }}
{% elseif query.end is not empty %}
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}
{% elseif query.begin is not empty %}
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}
{% endif %}
{% endif %}
{% if query.user is not empty %}
: {{ widgets.username(query.user) }}
{% endif %}
</span>
</h2>
</div>
</div>
<div class="row">
<div class="col-xs-12 table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
{% if query.user is empty %}
<th>{{ 'label.username'|trans }}</th>
{% endif %}
<th>{{ 'label.description'|trans }}</th>
{% for field in metaColumns %}
<th>{{ field.label|trans }}</th>
{% endfor %}
<th>{{ 'label.hours'|trans }}</th>
</tr>
</thead>
<tbody>
{% set timeWorked = 0 %}
{% for entry in entries %}
{% set timeWorked = timeWorked + entry.duration %}
<tr>
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
{% if query.user is empty %}
<td>{{ widgets.username(entry.user) }}</td>
{% endif %}
<td>
{% if entry.description is not empty %}
<div>
{{ entry.description|escape|desc2html }}
</div>
{% endif %}
<span class="small">
{{ 'label.activity'|trans }}: {{ entry.activity.name }} |
{{ 'label.project'|trans }}: {{ entry.project.name }} |
{{ 'label.customer'|trans }}: {{ entry.project.customer.name }}
</span>
</td>
{% for field in metaColumns %}
<td>{{ tables.datatable_meta_column(entry, field) }}</td>
{% endfor %}
<td class="text-nowrap">{{ entry.duration|duration }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th></th>
{% if query.user is empty %}
<th></th>
{% endif %}
{% for field in metaColumns %}
<th></th>
{% endfor %}
<th>{{ 'invoice.total_working_time'|trans }}</th>
<th>{{ timeWorked|duration }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
{% endblock %}
{% block print_button %}{% endblock %}

View File

@@ -3,7 +3,16 @@
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}} %}
{% if is_granted('export_own_timesheet') %}
{% set actions = actions|merge({'download': {'url': path('timesheet_export'), 'class': 'toolbar-action'}}) %}
{% set exporterIds = timesheet_exporter() %}
{% if exporterIds|length == 1 %}
{% set actions = actions|merge({'download': {'url': path('timesheet_export', {'exporter': exporterIds.0}), 'class': 'toolbar-action'}}) %}
{% elseif exporterIds|length > 1 %}
{% set children = {} %}
{% for exporter in exporterIds %}
{% set children = children|merge({(exporter): {'title': ('button.' ~ exporter)|trans, 'url': path('timesheet_export', {'exporter': exporter}), 'class': 'toolbar-action exporter-' ~ exporter}}) %}
{% endfor %}
{% set actions = actions|merge({'download': {'children': children}}) %}
{% endif %}
{% endif %}
{% set actions = actions|merge({'visibility': {'modal': '#modal_timesheet'}}) %}
{% if is_granted('create_own_timesheet') %}

View File

@@ -3,6 +3,10 @@
{% extends 'invoice/layout.html.twig' %}
{% block invoice %}
{% set showUserColumn = true %}
{% if query.user %}
{% set showUserColumn = false %}
{% endif %}
<div class="row">
<div class="col-xs-12">
@@ -10,16 +14,20 @@
<span contenteditable="true">
{% if query.begin is not empty and query.end is not empty %}
{% if query.begin|date('m') != query.end|date('m') or query.begin|date('Y') != query.end|date('Y') %}
{{ query.begin|date_short }} - {{ query.end|date_short }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
{% elseif query.end is not empty %}
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}:
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}
{% elseif query.begin is not empty %}
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}:
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}
{% endif %}
{% endif %}
{{ widgets.username(query.user) }}
</span>
</h2>
{% if not showUserColumn %}
<p>
{{ 'label.user'|trans }}: {{ widgets.username(query.user) }}
</p>
{% endif %}
</div>
</div>
@@ -29,6 +37,9 @@
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
{% if showUserColumn %}
<th>{{ 'label.username'|trans }}</th>
{% endif %}
<th>{{ 'label.description'|trans }}</th>
{% for field in metaColumns %}
<th>{{ field.label|trans }}</th>
@@ -42,11 +53,14 @@
{% set timeWorked = timeWorked + entry.duration %}
<tr>
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
{% if showUserColumn %}
<td>{{ widgets.username(entry.user) }}</td>
{% endif %}
<td>
{% if entry.description is not empty %}
<div>
{{ entry.description|escape|desc2html }}
</div>
<div>
{{ entry.description|escape|desc2html }}
</div>
{% endif %}
<span class="small">
{{ 'label.activity'|trans }}: {{ entry.activity.name }} |
@@ -64,6 +78,9 @@
<tfoot>
<tr>
<th></th>
{% if showUserColumn %}
<th></th>
{% endif %}
{% for field in metaColumns %}
<th></th>
{% endfor %}

View File

@@ -30,7 +30,7 @@ class InstallCommandTest extends KernelTestCase
protected function getCommand($permission = 0777): Command
{
$fileMock = $this->getMockBuilder(File::class)->setMethods(['getPermissions'])->getMock();
$fileMock = $this->getMockBuilder(File::class)->onlyMethods(['getPermissions'])->getMock();
$fileMock->expects($this->exactly(5))->method('getPermissions')->willReturn($permission);
$kernel = self::bootKernel();

View File

@@ -29,11 +29,11 @@ class PluginCommandTest extends KernelTestCase
public function testWithPlugins()
{
$plugin1 = $this->getMockBuilder(PluginInterface::class)->setMethods(['getName', 'getPath'])->getMock();
$plugin1 = $this->getMockBuilder(PluginInterface::class)->onlyMethods(['getName', 'getPath'])->getMock();
$plugin1->expects($this->exactly(3))->method('getName')->willReturn('Test-Bundle');
$plugin1->expects($this->once())->method('getPath')->willReturn(__DIR__);
$plugin2 = $this->getMockBuilder(PluginInterface::class)->setMethods(['getName', 'getPath'])->getMock();
$plugin2 = $this->getMockBuilder(PluginInterface::class)->onlyMethods(['getName', 'getPath'])->getMock();
$plugin2->expects($this->exactly(3))->method('getName')->willReturn('Another one');
$plugin2->expects($this->once())->method('getPath')->willReturn('BundleDirectory');

View File

@@ -207,7 +207,7 @@ abstract class ControllerBaseTest extends WebTestCase
*/
protected function assertPageActions(Client $client, array $buttons)
{
$node = $client->getCrawler()->filter('section.content-header div.breadcrumb div.box-tools div.btn-group a.btn');
$node = $client->getCrawler()->filter('section.content-header div.breadcrumb div.box-tools div.btn-group a');
/** @var \DOMElement $element */
foreach ($node->getIterator() as $element) {

View File

@@ -36,7 +36,10 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertHasNoEntriesWithFilter($client);
$this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#',
'download toolbar-action' => $this->createUrl('/timesheet/export'),
'toolbar-action exporter-csv' => $this->createUrl('/timesheet/export/csv'),
'toolbar-action exporter-print' => $this->createUrl('/timesheet/export/print'),
'toolbar-action exporter-pdf' => $this->createUrl('/timesheet/export/pdf'),
'toolbar-action exporter-xlsx' => $this->createUrl('/timesheet/export/xlsx'),
'visibility' => '#',
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
'help' => 'https://www.kimai.org/documentation/timesheet.html'
@@ -133,7 +136,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/timesheet/export'));
$form->getFormNode()->setAttribute('action', $this->createUrl('/timesheet/export/print'));
$client->submit($form, [
'state' => 1,
'pageSize' => 25,

View File

@@ -37,7 +37,10 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#',
'download toolbar-action' => $this->createUrl('/team/timesheet/export'),
'toolbar-action exporter-csv' => $this->createUrl('/team/timesheet/export/csv'),
'toolbar-action exporter-print' => $this->createUrl('/team/timesheet/export/print'),
'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'),
'toolbar-action exporter-xlsx' => $this->createUrl('/team/timesheet/export/xlsx'),
'visibility' => '#',
'create modal-ajax-form' => $this->createUrl('/team/timesheet/create'),
'help' => 'https://www.kimai.org/documentation/timesheet.html'
@@ -142,7 +145,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export'));
$form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export/print'));
$client->submit($form, [
'state' => 1,
'pageSize' => 25,

View File

@@ -34,16 +34,13 @@ class SqliteSessionInitSubscriberTest extends TestCase
$sut = new SqliteSessionInitSubscriber();
$platformMock = $this->getMockBuilder(SqlitePlatform::class)
->setMethods(['getName'])
->onlyMethods(['getName'])
->disableOriginalConstructor()
->getMock();
$platformMock->expects($this->once())->method('getName')->willReturn('sqlite');
$connectionMock = $this->getMockBuilder(Connection::class)
->setMethods(['getDatabasePlatform', 'getConnection', 'exec'])
->disableOriginalConstructor()
->getMock();
$connectionMock = $this->createMock(Connection::class);
$connectionMock->expects($this->once())->method('getDatabasePlatform')->willReturn($platformMock);
$connectionMock->expects($this->once())->method('exec')->with('PRAGMA foreign_keys = ON;');
@@ -57,16 +54,13 @@ class SqliteSessionInitSubscriberTest extends TestCase
$sut = new SqliteSessionInitSubscriber();
$platformMock = $this->getMockBuilder(MySqlPlatform::class)
->setMethods(['getName'])
->onlyMethods(['getName'])
->disableOriginalConstructor()
->getMock();
$platformMock->expects($this->once())->method('getName')->willReturn('mysql');
$connectionMock = $this->getMockBuilder(Connection::class)
->setMethods(['getDatabasePlatform', 'getConnection', 'exec'])
->disableOriginalConstructor()
->getMock();
$connectionMock = $this->createMock(Connection::class);
$connectionMock->expects($this->once())->method('getDatabasePlatform')->willReturn($platformMock);
$connectionMock->expects($this->never())->method('exec')->with('PRAGMA foreign_keys = ON;');

View File

@@ -46,7 +46,7 @@ class RegistrationSubscriberTest extends TestCase
$form = $this->getMockBuilder(FormInterface::class)->getMock();
$form->method('getData')->willReturn($user);
$request = $this->getMockBuilder(Request::class)->getMock();
$request = $this->createMock(Request::class);
$event = new FormEvent($form, $request);

View File

@@ -35,6 +35,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTest extends KernelTestCase
@@ -60,13 +61,16 @@ abstract class AbstractRendererTest extends KernelTestCase
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages));
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$translator = $this->createMock(TranslatorInterface::class);
$dateExtension = new DateExtensions($localeSettings);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
return new $classname($translator, $dateExtension, $dispatcher);
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
$authMock->method('isGranted')->willReturn(true);
return new $classname($translator, $dateExtension, $dispatcher, $authMock);
}
/**
@@ -91,12 +95,12 @@ abstract class AbstractRendererTest extends KernelTestCase
$activity->setMetaField((new ActivityMeta())->setName('activity-foo')->setValue('activity-bar')->setIsVisible(true));
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user2 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2->method('getId')->willReturn(2);
$user2->method('getUsername')->willReturn('hello-world');

View File

@@ -13,6 +13,9 @@ use App\Export\Renderer\CsvRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\CsvRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\CsvRenderer
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
* @covers \App\Export\Renderer\RendererTrait

View File

@@ -15,6 +15,8 @@ use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
/**
* @covers \App\Export\Base\HtmlRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\HtmlRenderer
* @covers \App\Export\Renderer\RendererTrait
* @group integration
@@ -24,7 +26,7 @@ class HtmlRendererTest extends AbstractRendererTest
public function testConfiguration()
{
$sut = new HtmlRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
$this->createMock(Environment::class),
new EventDispatcher()
);

View File

@@ -17,6 +17,8 @@ use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
/**
* @covers \App\Export\Base\PDFRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\PDFRenderer
* @covers \App\Export\Renderer\RendererTrait
* @group integration
@@ -31,9 +33,9 @@ class PdfRendererTest extends AbstractRendererTest
public function testConfiguration()
{
$sut = new PDFRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
$this->createMock(Environment::class),
$this->getDateTimeFactory(),
$this->getMockBuilder(HtmlToPdfConverter::class)->getMock()
$this->createMock(HtmlToPdfConverter::class)
);
$this->assertEquals('pdf', $sut->getId());

View File

@@ -13,6 +13,9 @@ use App\Export\Renderer\XlsxRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\XlsxRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\XlsxRenderer
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
* @covers \App\Export\Renderer\RendererTrait

View File

@@ -11,6 +11,7 @@ namespace App\Tests\Export;
use App\Export\Renderer\HtmlRenderer;
use App\Export\ServiceExport;
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Twig\Environment;
@@ -22,36 +23,34 @@ class ServiceExportTest extends TestCase
{
public function testEmptyObject()
{
$sut = new ServiceExport([]);
$this->assertEmpty($sut->getRenderer());
$sut = new ServiceExport();
self::assertEmpty($sut->getRenderer());
self::assertNull($sut->getRendererById('default'));
self::assertEmpty($sut->getTimesheetExporter());
self::assertNull($sut->getTimesheetExporterById('default'));
}
public function testUnknownRendererReturnsNull()
public function testAddRenderer()
{
$sut = new ServiceExport([]);
$this->assertNull($sut->getRendererById('default'));
$sut = new ServiceExport();
$renderer = new HtmlRenderer($this->createMock(Environment::class), new EventDispatcher());
$sut->addRenderer($renderer);
self::assertEquals(1, count($sut->getRenderer()));
self::assertSame($renderer, $sut->getRendererById('html'));
}
public function testAdd()
public function testAddTimesheetExporter()
{
$sut = new ServiceExport([]);
$sut = new ServiceExport();
$sut->addRenderer(new HtmlRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
new EventDispatcher()
));
$exporter = new HtmlExporter($this->createMock(Environment::class), new EventDispatcher());
$sut->addTimesheetExporter($exporter);
$this->assertEquals(1, count($sut->getRenderer()));
}
public function testAddViaConstruct()
{
$renderer = [new HtmlRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
new EventDispatcher()
)];
$sut = new ServiceExport($renderer);
$this->assertEquals(1, count($sut->getRenderer()));
self::assertEquals(1, count($sut->getTimesheetExporter()));
self::assertSame($exporter, $sut->getTimesheetExporterById('print'));
}
}

View File

@@ -0,0 +1,221 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Configuration\LanguageFormattings;
use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\Customer;
use App\Entity\CustomerMeta;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\ProjectMeta;
use App\Entity\Tag;
use App\Entity\Timesheet;
use App\Entity\TimesheetMeta;
use App\Entity\User;
use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Utils\LocaleSettings;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTest extends KernelTestCase
{
/**
* @param string $classname
* @return TimesheetExportInterface
*/
protected function getAbstractRenderer(string $classname)
{
$requestStack = new RequestStack();
$languages = [
'en' => [
'date' => 'Y.m.d',
'duration' => '%h:%m h',
'time' => 'H:i',
]
];
$request = new Request();
$request->setLocale('en');
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages));
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new DateExtensions($localeSettings);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
$authMock->method('isGranted')->willReturn(true);
return new $classname($translator, $dateExtension, $dispatcher, $authMock);
}
/**
* @param TimesheetExportInterface $renderer
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function render(TimesheetExportInterface $renderer)
{
$customer = new Customer();
$customer->setName('Customer Name');
$customer->setMetaField((new CustomerMeta())->setName('customer-foo')->setValue('customer-bar')->setIsVisible(true));
$project = new Project();
$project->setName('project name');
$project->setCustomer($customer);
$project->setMetaField((new ProjectMeta())->setName('project-bar')->setValue('project-bar')->setIsVisible(true));
$project->setMetaField((new ProjectMeta())->setName('project-foo2')->setValue('project-foo2')->setIsVisible(true));
$activity = new Activity();
$activity->setName('activity description');
$activity->setProject($project);
$activity->setMetaField((new ActivityMeta())->setName('activity-foo')->setValue('activity-bar')->setIsVisible(true));
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2->method('getId')->willReturn(2);
$user2->method('getUsername')->willReturn('hello-world');
$timesheet = new Timesheet();
$timesheet
->setDuration(3600)
->setRate(293.27)
->setUser($user1)
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$timesheet2 = new Timesheet();
$timesheet2
->setDuration(400)
->setRate(84.75)
->setUser($user2)
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$timesheet3 = new Timesheet();
$timesheet3
->setDuration(1800)
->setRate(111.11)
->setUser($user1)
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$timesheet4 = new Timesheet();
$timesheet4
->setDuration(400)
->setRate(1947.99)
->setUser($user2)
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
->addTag((new Tag())->setName('foo'))
;
$timesheet5 = new Timesheet();
$timesheet5
->setDuration(400)
->setFixedRate(84)
->setUser((new User())->setUsername('kevin'))
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime('2019-06-16 12:00:00'))
->setEnd(new \DateTime('2019-06-16 12:06:40'))
->addTag((new Tag())->setName('foo'))
->addTag((new Tag())->setName('bar'))
->setMetaField((new TimesheetMeta())->setName('foo')->setValue('meta-bar')->setIsVisible(true))
->setMetaField((new TimesheetMeta())->setName('foo2')->setValue('meta-bar2')->setIsVisible(true))
;
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
$query = new TimesheetQuery();
$query->setActivity($activity);
$query->setBegin(new \DateTime());
$query->setEnd(new \DateTime());
$query->setProject($project);
return $renderer->render($entries, $query);
}
}
class MetaFieldColumnSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
TimesheetMetaDisplayEvent::class => ['loadTimesheetField', 200],
CustomerMetaDisplayEvent::class => ['loadCustomerField', 200],
ProjectMetaDisplayEvent::class => ['loadProjectField', 200],
ActivityMetaDisplayEvent::class => ['loadActivityField', 200],
];
}
public function loadTimesheetField(TimesheetMetaDisplayEvent $event)
{
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo'));
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo2'));
}
public function loadCustomerField(CustomerMetaDisplayEvent $event)
{
$event->addField($this->prepareEntity(new CustomerMeta(), 'customer-foo'));
}
public function loadProjectField(ProjectMetaDisplayEvent $event)
{
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo'));
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo2')->setIsVisible(false));
}
public function loadActivityField(ActivityMetaDisplayEvent $event)
{
$event->addField($this->prepareEntity(new ActivityMeta(), 'activity-foo'));
}
private function prepareEntity(MetaTableTypeInterface $meta, string $name)
{
return $meta
->setLabel('Working place')
->setName($name)
->setType(TextType::class)
->setIsVisible(true);
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\CsvRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\CsvRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\CsvRenderer
* @covers \App\Export\Timesheet\AbstractSpreadsheetRenderer
* @covers \App\Export\Timesheet\RendererTrait
* @group integration
*/
class CsvRendererTest extends AbstractRendererTest
{
public function testConfiguration()
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
$this->assertEquals('csv', $sut->getId());
}
public function getTestModel()
{
return [
['400', '2437.12', ' EUR 1,947.99 ', 7, 5, 1, 2, 2]
];
}
/**
* @dataProvider getTestModel
*/
public function testRender($totalDuration, $totalRate, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3)
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
/** @var BinaryFileResponse $response */
$response = $this->render($sut);
$file = $response->getFile();
$this->assertEquals('text/csv', $response->headers->get('Content-Type'));
$this->assertEquals('attachment; filename=kimai-export.csv', $response->headers->get('Content-Disposition'));
$this->assertTrue(file_exists($file->getRealPath()));
$content = file_get_contents($file->getRealPath());
$this->assertStringContainsString('"' . $totalDuration . '"', $content);
$this->assertStringContainsString('"' . $totalRate . '"', $content);
$this->assertStringContainsString('"' . $expectedRate . '"', $content);
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
$this->assertEquals($expectedUser1, substr_count($content, ',"kevin",'));
$this->assertEquals($expectedUser3, substr_count($content, ',"hello-world",'));
$this->assertEquals($expectedUser2, substr_count($content, ',"foo-bar",'));
ob_start();
$response->sendContent();
$content2 = ob_get_clean();
$this->assertEquals($content, $content2);
$this->assertFalse(file_exists($file->getRealPath()));
$all = [];
$rows = str_getcsv($content2, PHP_EOL);
foreach ($rows as $row) {
$all[] = str_getcsv($row);
}
$expected = [
0 => '2019-06-16',
1 => '12:00',
2 => '12:06',
3 => '400',
4 => '0',
5 => 'kevin',
6 => 'Customer Name',
7 => 'project name',
8 => 'activity description',
9 => '',
10 => '',
11 => 'foo,bar',
12 => '',
13 => ' EUR 84.00 ',
14 => 'meta-bar',
15 => 'meta-bar2',
16 => 'customer-bar',
17 => '',
18 => 'project-foo2',
19 => 'activity-bar',
];
self::assertEquals(7, count($all));
self::assertEquals($expected, $all[5]);
self::assertEquals(count($expected), count($all[0]));
self::assertEquals('foo', $all[4][11]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\HtmlRenderer;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
/**
* @covers \App\Export\Timesheet\HtmlRenderer
* @group integration
*/
class HtmlRendererTest extends AbstractRendererTest
{
public function testConfiguration()
{
$sut = new HtmlRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
new EventDispatcher()
);
$this->assertEquals('print', $sut->getId());
}
public function testRender()
{
$kernel = self::bootKernel();
/** @var Environment $twig */
$twig = $kernel->getContainer()->get('twig');
$stack = $kernel->getContainer()->get('request_stack');
$request = new Request();
$request->setLocale('en');
$stack->push($request);
$sut = new HtmlRenderer($twig, new EventDispatcher());
$response = $this->render($sut);
$content = $response->getContent();
$this->assertStringContainsString('<th>01:50 h</th>', $content);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\PDFRenderer;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Utils\HtmlToPdfConverter;
use App\Utils\MPdfConverter;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
/**
* @covers \App\Export\Base\PDFRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\PDFRenderer
* @covers \App\Export\Timesheet\RendererTrait
* @group integration
*/
class PdfRendererTest extends AbstractRendererTest
{
protected function getDateTimeFactory()
{
return (new UserDateTimeFactoryFactory($this))->create();
}
public function testConfiguration()
{
$sut = new PDFRenderer(
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
$this->getDateTimeFactory(),
$this->getMockBuilder(HtmlToPdfConverter::class)->getMock()
);
$this->assertEquals('pdf', $sut->getId());
}
public function testRender()
{
$kernel = self::bootKernel();
/** @var Environment $twig */
$twig = $kernel->getContainer()->get('twig');
$stack = $kernel->getContainer()->get('request_stack');
$cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
$converter = new MPdfConverter($cacheDir);
$request = new Request();
$request->setLocale('en');
$stack->push($request);
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter);
$response = $this->render($sut);
$this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
$this->assertEquals('attachment; filename=kimai-export.pdf', $response->headers->get('Content-Disposition'));
$this->assertNotEmpty($response->getContent());
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\XlsxRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\XlsxRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\XlsxRenderer
* @covers \App\Export\Timesheet\AbstractSpreadsheetRenderer
* @covers \App\Export\Timesheet\RendererTrait
* @group integration
*/
class XlsxRendererTest extends AbstractRendererTest
{
public function testConfiguration()
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
$this->assertEquals('xlsx', $sut->getId());
}
public function testRender()
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
/** @var BinaryFileResponse $response */
$response = $this->render($sut);
$file = $response->getFile();
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
$this->assertEquals('attachment; filename=kimai-export.xlsx', $response->headers->get('Content-Disposition'));
$this->assertTrue(file_exists($file->getRealPath()));
ob_start();
$response->sendContent();
$content2 = ob_get_clean();
$this->assertNotEmpty($content2);
$this->assertFalse(file_exists($file->getRealPath()));
}
}

View File

@@ -45,7 +45,7 @@ class TagArrayToStringTransformerTest extends TestCase
(new Tag())->setName('bar'),
];
$repository = $this->getMockBuilder(TagRepository::class)->setMethods(['findBy'])->disableOriginalConstructor()->getMock();
$repository = $this->getMockBuilder(TagRepository::class)->onlyMethods(['findBy'])->disableOriginalConstructor()->getMock();
$repository->expects($this->once())->method('findBy')->willReturn($results);
$sut = new TagArrayToStringTransformer($repository);

View File

@@ -50,15 +50,15 @@ abstract class AbstractCalculatorTest extends TestCase
$template = new InvoiceTemplate();
$template->setVat(19);
$user = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);
$project = $this->getMockBuilder(Project::class)->setMethods(['getId', 'getCustomer', 'getName'])->disableOriginalConstructor()->getMock();
$project = $this->getMockBuilder(Project::class)->onlyMethods(['getId', 'getCustomer', 'getName'])->disableOriginalConstructor()->getMock();
$project->method('getId')->willReturn(1);
$project->method('getCustomer')->willReturn($customer);
$project->method('getName')->willReturn('project description');
$activity = $this->getMockBuilder(Activity::class)->setMethods(['getId', 'getProject', 'getName'])->disableOriginalConstructor()->getMock();
$activity = $this->getMockBuilder(Activity::class)->onlyMethods(['getId', 'getProject', 'getName'])->disableOriginalConstructor()->getMock();
$activity->method('getId')->willReturn(1);
$activity->method('getProject')->willReturn($project);
$activity->method('getName')->willReturn('activity description');

View File

@@ -38,16 +38,16 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTest
$template = new InvoiceTemplate();
$template->setVat(19);
$user = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);
$activity1 = $this->getMockBuilder(Activity::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity1 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity1->method('getId')->willReturn(1);
$activity2 = $this->getMockBuilder(Activity::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity2 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity2->method('getId')->willReturn(2);
$activity3 = $this->getMockBuilder(Activity::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity3 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$activity3->method('getId')->willReturn(3);
$timesheet = new Timesheet();

View File

@@ -39,16 +39,16 @@ class DateInvoiceCalculatorTest extends AbstractCalculatorTest
$template = new InvoiceTemplate();
$template->setVat(19);
$user = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);
$project1 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project1 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project1->method('getId')->willReturn(1);
$project2 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project2 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project2->method('getId')->willReturn(2);
$project3 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project3 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project3->method('getId')->willReturn(3);
$timesheet = new Timesheet();

View File

@@ -39,16 +39,16 @@ class ProjectInvoiceCalculatorTest extends AbstractCalculatorTest
$template = new InvoiceTemplate();
$template->setVat(19);
$user = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);
$project1 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project1 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project1->method('getId')->willReturn(1);
$project2 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project2 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project2->method('getId')->willReturn(2);
$project3 = $this->getMockBuilder(Project::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$project3 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$project3->method('getId')->willReturn(3);
$timesheet = new Timesheet();

View File

@@ -40,13 +40,13 @@ class UserInvoiceCalculatorTest extends AbstractCalculatorTest
$activity = new Activity();
$activity->setName('activity description');
$user1 = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user1 = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user2 = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user2 = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user2->method('getId')->willReturn(2);
$user3 = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
$user3 = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock();
$user3->method('getId')->willReturn(3);
$timesheet = new Timesheet();

View File

@@ -103,12 +103,12 @@ trait RendererTestTrait
$activity->setMetaField((new ActivityMeta())->setName('foo-activity')->setValue('bar-activity')->setIsVisible(true));
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user2 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2->method('getId')->willReturn(2);
$user2->method('getUsername')->willReturn('hello-world');
@@ -219,7 +219,7 @@ trait RendererTestTrait
$activity->setMetaField((new ActivityMeta())->setName('foo-activity')->setValue('bar-activity')->setIsVisible(true));
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');

View File

@@ -67,7 +67,7 @@ class LdapAuthenticationProviderTest extends TestCase
$user = (new User())->setUsername('foo')->setEnabled(true);
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -84,10 +84,10 @@ class LdapAuthenticationProviderTest extends TestCase
$this->expectExceptionMessage('The presented password is invalid.');
$user = (new User())->setUsername('foo')->setEnabled(true);
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['bind'])->getMock();
$manager->expects($this->once())->method('bind')->willReturn(false);
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -104,10 +104,10 @@ class LdapAuthenticationProviderTest extends TestCase
$this->expectExceptionMessage('The credentials were changed from another session.');
$user = (new User())->setUsername('foo')->setEnabled(true);
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['bind'])->getMock();
$manager->expects($this->once())->method('bind')->willReturn(false);
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->never())->method('loadUserByUsername');
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -122,13 +122,13 @@ class LdapAuthenticationProviderTest extends TestCase
{
$user = (new User())->setUsername('foo')->setEnabled(true);
$user->setPreferenceValue('ldap.dn', 'blub');
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind', 'updateUser'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['bind', 'updateUser'])->getMock();
$manager->expects($this->once())->method('bind')->willReturn(true);
$manager->expects($this->once())->method('updateUser')->willReturnCallback(function ($updateUser) use ($user) {
self::assertSame($updateUser, $user);
});
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -144,13 +144,13 @@ class LdapAuthenticationProviderTest extends TestCase
{
$user = (new User())->setUsername('foo')->setEnabled(true);
$user->setPreferenceValue('ldap.dn', 'blub');
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind', 'updateUser'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['bind', 'updateUser'])->getMock();
$manager->expects($this->once())->method('bind')->willReturn(true);
$manager->expects($this->once())->method('updateUser')->willReturnCallback(function ($updateUser) use ($user) {
self::assertSame($updateUser, $user);
});
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->never())->method('loadUserByUsername');
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -169,7 +169,7 @@ class LdapAuthenticationProviderTest extends TestCase
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->once())->method('loadUserByUsername')->willThrowException(new UsernameNotFoundException('blub foo bar'));
$providerKey = 'secured_area';
$userChecker = new UserChecker();
@@ -188,7 +188,7 @@ class LdapAuthenticationProviderTest extends TestCase
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
$config = new LdapConfiguration([]);
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->onlyMethods(['loadUserByUsername'])->getMock();
$userProvider->expects($this->once())->method('loadUserByUsername')->willThrowException(new \Exception('server away', 1234));
$providerKey = 'secured_area';
$userChecker = new UserChecker();

View File

@@ -31,7 +31,7 @@ class LdapDriverTest extends TestCase
public function testBindSuccess()
{
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->onlyMethods(['bind'])->getMock();
$zendLdap->expects($this->once())->method('bind')->willReturnSelf();
$user = new User();
@@ -42,7 +42,7 @@ class LdapDriverTest extends TestCase
public function testBindException()
{
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->onlyMethods(['bind'])->getMock();
$zendLdap->expects($this->once())->method('bind')->willThrowException(new LdapException());
$user = new User();
@@ -53,7 +53,7 @@ class LdapDriverTest extends TestCase
public function testSearchSuccess()
{
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind', 'searchEntries'])->getMock();
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->onlyMethods(['bind', 'searchEntries'])->getMock();
$zendLdap->expects($this->once())->method('bind');
$zendLdap->expects($this->once())->method('searchEntries')->willReturn([1, 2, 3]);
@@ -67,7 +67,7 @@ class LdapDriverTest extends TestCase
$this->expectException(LdapDriverException::class);
$this->expectExceptionMessage('An error occurred with the search operation.');
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind', 'searchEntries'])->getMock();
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->onlyMethods(['bind', 'searchEntries'])->getMock();
$zendLdap->expects($this->once())->method('bind');
$zendLdap->expects($this->once())->method('searchEntries')->willThrowException(
new LdapException($zendLdap, '', LdapException::LDAP_SERVER_DOWN)

View File

@@ -65,7 +65,7 @@ class LdapManagerTest extends TestCase
'count' => 0
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
@@ -87,7 +87,7 @@ class LdapManagerTest extends TestCase
'count' => 3
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
@@ -106,7 +106,7 @@ class LdapManagerTest extends TestCase
'count' => 1,
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
@@ -125,7 +125,7 @@ class LdapManagerTest extends TestCase
'count' => 0
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
@@ -147,7 +147,7 @@ class LdapManagerTest extends TestCase
'count' => 3
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
@@ -166,7 +166,7 @@ class LdapManagerTest extends TestCase
'count' => 1,
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
self::assertEquals('(&(&(objectClass=inetOrgPerson))(träl=alß#\\\aa=XY\5cZ0)(test=fu=n))', $filter);
@@ -183,7 +183,7 @@ class LdapManagerTest extends TestCase
{
$user = (new User())->setUsername('foobar');
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['bind'])->getMock();
$driver->expects($this->once())->method('bind')->willReturnCallback(function ($bindUser, $password) use ($user) {
self::assertSame($user, $bindUser);
self::assertEquals('a-very-secret-secret', $password);
@@ -210,7 +210,7 @@ class LdapManagerTest extends TestCase
],
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
@@ -249,7 +249,7 @@ class LdapManagerTest extends TestCase
],
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
@@ -284,7 +284,7 @@ class LdapManagerTest extends TestCase
],
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
@@ -418,7 +418,7 @@ class LdapManagerTest extends TestCase
'count' => 4
];
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->onlyMethods(['search'])->getMock();
$driver->expects($this->exactly(3))->method('search')->willReturnCallback(function ($baseDn, $filter, $attributes) use ($expectedUsers, $expectedGroups, $expectedGroupQuery, $expected) {
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=Karl-Heinz))', $filter);

View File

@@ -25,7 +25,7 @@ class LdapUserProviderTest extends TestCase
$this->expectException(UsernameNotFoundException::class);
$this->expectExceptionMessage('User "test" not found');
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['findUserByUsername'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['findUserByUsername'])->getMock();
$manager->expects($this->once())->method('findUserByUsername')->willReturn(null);
$sut = new LdapUserProvider($manager);
@@ -37,7 +37,7 @@ class LdapUserProviderTest extends TestCase
$user = new User();
$user->setUsername('foobar');
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['findUserByUsername'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['findUserByUsername'])->getMock();
$manager->expects($this->once())->method('findUserByUsername')->willReturn($user);
$sut = new LdapUserProvider($manager);
@@ -52,7 +52,7 @@ class LdapUserProviderTest extends TestCase
$user->setUsername('foobar');
$user->setPreferenceValue('ldap.dn', 'sdfdsf');
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['updateUser'])->getMock();
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->onlyMethods(['updateUser'])->getMock();
$sut = new LdapUserProvider($manager);
$actual = $sut->refreshUser($user);

View File

@@ -34,9 +34,9 @@ class CurrentUserFactory extends AbstractMockFactory
$user->addPreference($pref);
}
$repository = $this->getMockBuilder(UserRepository::class)->setMethods(['getUserById'])->disableOriginalConstructor()->getMock();
$repository = $this->getMockBuilder(UserRepository::class)->onlyMethods(['getUserById'])->disableOriginalConstructor()->getMock();
$repository->expects(TestCase::atMost(1))->method('getUserById')->willReturn($user);
$token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock();
$token = $this->getMockBuilder(UsernamePasswordToken::class)->onlyMethods(['getUser'])->disableOriginalConstructor()->getMock();
$token->expects(TestCase::atLeast(1))->method('getUser')->willReturn($user);
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($token);

View File

@@ -39,7 +39,7 @@ class PluginManagerTest extends TestCase
$sut = new PluginManager([]);
$plugin = $this->getMockBuilder(PluginInterface::class)
->setMethods(['getName', 'getPath'])
->onlyMethods(['getName', 'getPath'])
->getMock();
$plugin->method('getName')->willReturn('foo');

View File

@@ -21,7 +21,7 @@ class QueryBuilderPaginatorTest extends TestCase
{
public function testPaginator()
{
$em = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock();
$em = $this->createMock(EntityManager::class);
$qb = new QueryBuilder($em);
$sut = new QueryBuilderPaginator($qb, 10);

View File

@@ -68,14 +68,14 @@ class BaseQueryTest extends TestCase
private function getFormBuilder(string $name)
{
return new FormBuilder($name, null, new EventDispatcher(), $this->getMockBuilder(FormFactoryInterface::class)->getMock(), []);
return new FormBuilder($name, null, new EventDispatcher(), $this->createMock(FormFactoryInterface::class), []);
}
protected function resetByFormError(BaseQuery $sut, array $invalidFields)
{
$formBuilder = $this->getFormBuilder('form');
$formBuilder->setCompound(true);
$formBuilder->setDataMapper($this->getMockBuilder(DataMapperInterface::class)->getMock());
$formBuilder->setDataMapper($this->createMock(DataMapperInterface::class));
$form = $formBuilder->getForm();

View File

@@ -24,7 +24,7 @@ class WidgetRepositoryTest extends TestCase
{
public function testHasWidget()
{
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repoMock = $this->createMock(TimesheetRepository::class);
$userMock = (new CurrentUserFactory($this))->create(new User());
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
@@ -38,7 +38,7 @@ class WidgetRepositoryTest extends TestCase
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot find widget "foo".');
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repoMock = $this->createMock(TimesheetRepository::class);
$userMock = (new CurrentUserFactory($this))->create(new User());
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
@@ -50,7 +50,7 @@ class WidgetRepositoryTest extends TestCase
$this->expectException(WidgetException::class);
$this->expectExceptionMessage('Unknown widget type "FooBar"');
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repoMock = $this->createMock(TimesheetRepository::class);
$userMock = (new CurrentUserFactory($this))->create(new User());
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => 'FooBar', 'user' => false]]);
@@ -62,7 +62,7 @@ class WidgetRepositoryTest extends TestCase
$this->expectException(WidgetException::class);
$this->expectExceptionMessage('Widget type "App\Widget\Type\CompoundChart" is not an instance of "App\Widget\Type\AbstractWidgetType"');
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repoMock = $this->createMock(TimesheetRepository::class);
$userMock = (new CurrentUserFactory($this))->create(new User());
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => CompoundChart::class, 'user' => false]]);
@@ -74,7 +74,7 @@ class WidgetRepositoryTest extends TestCase
*/
public function testGetWidget($data, $query, $dataType)
{
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repoMock = $this->createMock(TimesheetRepository::class);
$repoMock->method('getStatistic')->willReturn($data);
$userMock = (new CurrentUserFactory($this))->create(new User());

View File

@@ -22,7 +22,7 @@ class MarkdownExtensionTest extends TestCase
{
public function testGetFilters()
{
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]);
$sut = new MarkdownExtension(new Markdown(), $config);
$filters = $sut->getFilters();
@@ -34,7 +34,7 @@ class MarkdownExtensionTest extends TestCase
public function testMarkdownToHtml()
{
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]);
$sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
@@ -43,7 +43,7 @@ class MarkdownExtensionTest extends TestCase
public function testTimesheetContent()
{
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => false]);
$sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals(

View File

@@ -24,7 +24,7 @@ class WidgetExtensionTest extends TestCase
{
protected function getSut($hasWidget = null, $getWidget = null, $renderer = null): WidgetExtension
{
$service = $this->getMockBuilder(WidgetService::class)->disableOriginalConstructor()->setMethods(['hasWidget', 'getWidget', 'findRenderer'])->getMock();
$service = $this->getMockBuilder(WidgetService::class)->disableOriginalConstructor()->onlyMethods(['hasWidget', 'getWidget', 'findRenderer'])->getMock();
if (null !== $hasWidget) {
$service->expects($this->once())->method('hasWidget')->willReturn($hasWidget);
}

View File

@@ -34,7 +34,7 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
$authMock->method('isGranted')->willReturn($isGranted);
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, [
'rules' => [
'allow_future_times' => false,

View File

@@ -24,7 +24,7 @@ class CompoundChartRendererTest extends TestCase
{
public function testSupports()
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->createMock(Environment::class);
$sut = new CompoundChartRenderer($twig);
self::assertTrue($sut->supports(new CompoundChart()));
self::assertFalse($sut->supports(new CompoundRow()));
@@ -32,7 +32,7 @@ class CompoundChartRendererTest extends TestCase
public function testRenderWithCounter()
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->onlyMethods(['render'])->getMock();
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
return json_encode([$name, $options]);
});

View File

@@ -24,7 +24,7 @@ class CompoundRowRendererTest extends TestCase
{
public function testSupports()
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->createMock(Environment::class);
$sut = new CompoundRowRenderer($twig);
self::assertTrue($sut->supports(new CompoundRow()));
self::assertFalse($sut->supports(new CompoundChart()));
@@ -32,7 +32,7 @@ class CompoundRowRendererTest extends TestCase
public function testRenderWithCounter()
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->onlyMethods(['render'])->getMock();
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
return json_encode([$name, $options]);
});

View File

@@ -24,7 +24,7 @@ class SimpleWidgetRendererTest extends TestCase
{
public function testSupports()
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->createMock(Environment::class);
$sut = new SimpleWidgetRenderer($twig);
self::assertTrue($sut->supports(new SimpleWidget()));
}
@@ -34,7 +34,7 @@ class SimpleWidgetRendererTest extends TestCase
*/
public function testRenderWithCounter(SimpleWidget $widget, $template, $color)
{
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->onlyMethods(['render'])->getMock();
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
return json_encode([$name, $options]);
});

View File

@@ -29,7 +29,7 @@ class DailyWorkingTimeChartTest extends TestCase
{
public function createSut(): AbstractWidgetType
{
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
$repository = $this->createMock(TimesheetRepository::class);
$mockFactory = new UserDateTimeFactoryFactory($this);
$userFactory = new CurrentUserFactory($this);
$user = $userFactory->create(new User(), 'Europe/Berlin');
@@ -100,7 +100,7 @@ class DailyWorkingTimeChartTest extends TestCase
public function testGetData()
{
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->setMethods(['getDailyData'])->getMock();
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->onlyMethods(['getDailyData'])->getMock();
$repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($user, $begin, $end) {
return [
['year' => '2019', 'month' => '1', 'day' => 1, 'rate' => 13.75, 'duration' => 1234]

View File

@@ -25,7 +25,7 @@ class WidgetServiceTest extends TestCase
{
public function testConstruct()
{
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
$repository = $this->createMock(WidgetRepository::class);
$sut = new WidgetService($repository, []);
self::assertFalse($sut->hasWidget('sdfsdf'));
@@ -39,7 +39,7 @@ class WidgetServiceTest extends TestCase
public function testFindRenderer()
{
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
$repository = $this->createMock(WidgetRepository::class);
$renderer = new SimpleWidgetRenderer(new Environment(new FilesystemLoader()));
$sut = new WidgetService($repository, [$renderer]);
@@ -56,7 +56,7 @@ class WidgetServiceTest extends TestCase
$this->expectException(WidgetException::class);
$this->expectExceptionMessage('No renderer available for widget "App\Widget\Type\More"');
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
$repository = $this->createMock(WidgetRepository::class);
$sut = new WidgetService($repository, []);
$sut->findRenderer(new More());
@@ -66,7 +66,7 @@ class WidgetServiceTest extends TestCase
{
$widget = new More();
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->setMethods(['has', 'get'])->getMock();
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->onlyMethods(['has', 'get'])->getMock();
$repository->expects($this->once())->method('has')->willReturn(true);
$repository->expects($this->once())->method('get')->willReturn($widget);