toolbar dropdown and visibility improvements (#933)

This commit is contained in:
Kevin Papst
2019-07-09 16:34:11 +02:00
committed by GitHub
parent b833e1ce27
commit c33a87a07c
74 changed files with 1317 additions and 631 deletions

View File

@@ -89,9 +89,6 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
jQuery(html).find('#form_modal .modal-content') jQuery(html).find('#form_modal .modal-content')
); );
// TODO these should be handled with Events, probably using Custom Events
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
// activate new loaded widgets // activate new loaded widgets
self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier); self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier);
self.getContainer().getPlugin('autocomplete').activateAutocomplete(formIdentifier + " .js-autocomplete"); self.getContainer().getPlugin('autocomplete').activateAutocomplete(formIdentifier + " .js-autocomplete");

View File

@@ -47,7 +47,6 @@ export default class KimaiDatatable extends KimaiPlugin {
} }
reloadDatatable() { reloadDatatable() {
// FIXME remove query
const durations = this.getContainer().getPlugin('timesheet-duration'); const durations = this.getContainer().getPlugin('timesheet-duration');
const form = jQuery('.toolbar form'); const form = jQuery('.toolbar form');
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>'; let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';

View File

@@ -67,10 +67,44 @@ export default class KimaiSelectDataAPI extends KimaiPlugin {
select.append('<option value="">' + emptyOption.text() + '</option>'); select.append('<option value="">' + emptyOption.text() + '</option>');
} }
jQuery.each(data, function(i, obj) { const options = {};
select.append('<option value="' + obj.id + '">' + obj.name + '</option>'); for (const apiData of data) {
let title = apiData.parentTitle;
if (title === null) {
title = '__empty__';
}
if (!options.hasOwnProperty(title)) {
options[title] = [];
}
options[title].push(apiData);
}
const ordered = {};
Object.keys(options).sort().forEach(function(key) {
ordered[key] = options[key];
}); });
let htmlOptions = '';
let emptyOptions = '';
for (const [key, value] of Object.entries(ordered)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
continue;
}
htmlOptions += '<optgroup label="' + key + '">';
for (const entity of value) {
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
htmlOptions += '</optgroup>';
}
select.append(htmlOptions);
select.append(emptyOptions);
// if we don't trigger the change, the other selects won't be resetted // if we don't trigger the change, the other selects won't be resetted
select.trigger('change'); select.trigger('change');

View File

@@ -35,6 +35,11 @@ App\Entity\Activity:
metaFields: metaFields:
exclude: true exclude: true
virtual_properties: virtual_properties:
parentTitle:
serialized_name: parentTitle
include: true
exp: "object.getProject() === null ? null : object.getProject().getName()"
groups: [Default]
getProject: getProject:
serialized_name: project serialized_name: project
exp: "object.getProject() === null ? null : object.getProject().getId()" exp: "object.getProject() === null ? null : object.getProject().getId()"

View File

@@ -33,6 +33,11 @@ App\Entity\Project:
metaFields: metaFields:
exclude: true exclude: true
virtual_properties: virtual_properties:
parentTitle:
serialized_name: parentTitle
include: true
exp: "object.getCustomer() === null ? null : object.getCustomer().getName()"
groups: [Default]
getCustomer: getCustomer:
serialized_name: customer serialized_name: customer
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()" exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"build/runtime.784b5d0d.js", "build/runtime.784b5d0d.js",
"build/0.66b5fd0b.js", "build/0.66b5fd0b.js",
"build/1.93f0cf2b.js", "build/1.93f0cf2b.js",
"build/app.0ac6f178.js" "build/app.3f742878.js"
], ],
"css": [ "css": [
"build/app.7f4a18a1.css" "build/app.7f4a18a1.css"
@@ -35,7 +35,7 @@
"build/runtime.784b5d0d.js": "sha384-+AsC7zoefPga1AkczlNSCXcbGwD0TwokkldvNISHf1nMlOpafanLjtKvlZy9cVsm", "build/runtime.784b5d0d.js": "sha384-+AsC7zoefPga1AkczlNSCXcbGwD0TwokkldvNISHf1nMlOpafanLjtKvlZy9cVsm",
"build/0.66b5fd0b.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok", "build/0.66b5fd0b.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
"build/1.93f0cf2b.js": "sha384-pmnwzvj+UJhDD3uDf+ekLARMBRtMDxWGbnJDrJvwIoRZd3MW/tOGzAW5Ybhbvw20", "build/1.93f0cf2b.js": "sha384-pmnwzvj+UJhDD3uDf+ekLARMBRtMDxWGbnJDrJvwIoRZd3MW/tOGzAW5Ybhbvw20",
"build/app.0ac6f178.js": "sha384-eORpGh3VKK5IISACV4rmp8CpkZ+s47G8aKaZnXVpswgZccU/YcJPkSa5+jhcI88T", "build/app.3f742878.js": "sha384-7221dBEmG8aOmSRCEjuKlrVH47VH1bdnmdEC2pb9y1d9Jtbg6d2t2HfyV3Wa34TI",
"build/app.7f4a18a1.css": "sha384-p8Dw7CP1RxxJ2K5yuaNpCw8r1lKd64DlVA2IQeEGxBAC2QxOjweafd8w9CSMk0GC", "build/app.7f4a18a1.css": "sha384-p8Dw7CP1RxxJ2K5yuaNpCw8r1lKd64DlVA2IQeEGxBAC2QxOjweafd8w9CSMk0GC",
"build/2.c06cd055.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q", "build/2.c06cd055.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
"build/chart.2bb71e2e.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y", "build/chart.2bb71e2e.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",

View File

@@ -3,7 +3,7 @@
"build/1.93f0cf2b.js": "build/1.93f0cf2b.js", "build/1.93f0cf2b.js": "build/1.93f0cf2b.js",
"build/2.c06cd055.js": "build/2.c06cd055.js", "build/2.c06cd055.js": "build/2.c06cd055.js",
"build/app.css": "build/app.7f4a18a1.css", "build/app.css": "build/app.7f4a18a1.css",
"build/app.js": "build/app.0ac6f178.js", "build/app.js": "build/app.3f742878.js",
"build/calendar.css": "build/calendar.b0551848.css", "build/calendar.css": "build/calendar.b0551848.css",
"build/calendar.js": "build/calendar.59417f4c.js", "build/calendar.js": "build/calendar.59417f4c.js",
"build/chart.js": "build/chart.2bb71e2e.js", "build/chart.js": "build/chart.2bb71e2e.js",

View File

@@ -62,7 +62,7 @@ class ActivityController extends BaseApiController
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, all activities will be returned.") * @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, all activities will be returned.")
* @Rest\QueryParam(name="visible", requirements="1|2|3", strict=true, nullable=true, description="Visibility status to filter activities. Allowed values: 1=visible, 2=hidden, 3=all (default: 1)") * @Rest\QueryParam(name="visible", requirements="1|2|3", strict=true, nullable=true, description="Visibility status to filter activities. Allowed values: 1=visible, 2=hidden, 3=all (default: 1)")
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Use if you want to fetch only global activities. Allowed values: true (default: false)") * @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Use if you want to fetch only global activities. Allowed values: true (default: false)")
* @Rest\QueryParam(name="globalsFirst", requirements="false", strict=true, nullable=true, description="Use if you don't want global activities to be listed first. Allowed values: false (default: true)") * @Rest\QueryParam(name="globalsFirst", requirements="true|false", strict=true, nullable=true, description="Deprecated parameter, value is not used any more")
* @Rest\QueryParam(name="orderBy", requirements="id|name|project", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, project (default: name)") * @Rest\QueryParam(name="orderBy", requirements="id|name|project", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, project (default: name)")
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)") * @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
* *
@@ -71,10 +71,6 @@ class ActivityController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher) public function cgetAction(ParamFetcherInterface $paramFetcher)
{ {
$query = new ActivityQuery(); $query = new ActivityQuery();
$query->setOrderGlobalsFirst(true)
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS)
->setOrderBy('name')
;
if (null !== ($order = $paramFetcher->get('order'))) { if (null !== ($order = $paramFetcher->get('order'))) {
$query->setOrder($order); $query->setOrder($order);
@@ -88,8 +84,8 @@ class ActivityController extends BaseApiController
$query->setGlobalsOnly(true); $query->setGlobalsOnly(true);
} }
if ('false' === $paramFetcher->get('globalsFirst')) { if (null !== $paramFetcher->get('globalsFirst')) {
$query->setOrderGlobalsFirst(false); @trigger_error('API parameter globalsFirst is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
} }
if (!empty($project = $paramFetcher->get('project'))) { if (!empty($project = $paramFetcher->get('project'))) {
@@ -100,7 +96,7 @@ class ActivityController extends BaseApiController
$query->setVisibility($visible); $query->setVisibility($visible);
} }
$data = $this->repository->findByQuery($query); $data = $this->repository->getActivitiesForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Activity']); $view->getContext()->setGroups(['Default', 'Collection', 'Activity']);

View File

@@ -68,10 +68,6 @@ class CustomerController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher) public function cgetAction(ParamFetcherInterface $paramFetcher)
{ {
$query = new CustomerQuery(); $query = new CustomerQuery();
$query
->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS)
->setOrderBy('name')
;
if (null !== ($order = $paramFetcher->get('order'))) { if (null !== ($order = $paramFetcher->get('order'))) {
$query->setOrder($order); $query->setOrder($order);
@@ -85,7 +81,7 @@ class CustomerController extends BaseApiController
$query->setVisibility($visible); $query->setVisibility($visible);
} }
$data = $this->repository->findByQuery($query); $data = $this->repository->getCustomersForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Customer']); $view->getContext()->setGroups(['Default', 'Collection', 'Customer']);

View File

@@ -70,10 +70,6 @@ class ProjectController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher) public function cgetAction(ParamFetcherInterface $paramFetcher)
{ {
$query = new ProjectQuery(); $query = new ProjectQuery();
$query
->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS)
->setOrderBy('name')
;
if (null !== ($order = $paramFetcher->get('order'))) { if (null !== ($order = $paramFetcher->get('order'))) {
$query->setOrder($order); $query->setOrder($order);
@@ -91,7 +87,7 @@ class ProjectController extends BaseApiController
$query->setVisibility($visible); $query->setVisibility($visible);
} }
$data = $this->repository->findByQuery($query); $data = $this->repository->getProjectsForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Project']); $view->getContext()->setGroups(['Default', 'Collection', 'Project']);

View File

@@ -127,7 +127,6 @@ class TimesheetController extends BaseApiController
{ {
$query = new TimesheetQuery(); $query = new TimesheetQuery();
$query->setUser($this->getUser()); $query->setUser($this->getUser());
$query->setResultType(TimesheetQuery::RESULT_TYPE_PAGER);
if ($this->isGranted('view_other_timesheet') && null !== ($user = $paramFetcher->get('user'))) { if ($this->isGranted('view_other_timesheet') && null !== ($user = $paramFetcher->get('user'))) {
if ('all' === $user) { if ('all' === $user) {

View File

@@ -16,6 +16,7 @@ use App\Form\ActivityEditForm;
use App\Form\Toolbar\ActivityToolbarForm; use App\Form\Toolbar\ActivityToolbarForm;
use App\Form\Type\ActivityType; use App\Form\Type\ActivityType;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\ActivityQuery; use App\Repository\Query\ActivityQuery;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
@@ -67,26 +68,19 @@ class ActivityController extends AbstractController
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
$query = new ActivityQuery(); $query = new ActivityQuery();
$query $query->setPage($page);
->setOrderBy('name')
->setExclusiveVisibility(true)
->setPage($page)
;
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var ActivityQuery $query */
$query = $form->getData();
}
/* @var $entries Pagerfanta */ /* @var $entries Pagerfanta */
$entries = $this->getRepository()->findByQuery($query); $entries = $this->getRepository()->getPagerfantaForQuery($query);
return $this->render('activity/index.html.twig', [ return $this->render('activity/index.html.twig', [
'entries' => $entries, 'entries' => $entries,
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $query->isDirty(),
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
]); ]);
} }
@@ -160,16 +154,11 @@ class ActivityController extends AbstractController
->add('activity', ActivityType::class, [ ->add('activity', ActivityType::class, [
'label' => 'label.activity', 'label' => 'label.activity',
'query_builder' => function (ActivityRepository $repo) use ($activity) { 'query_builder' => function (ActivityRepository $repo) use ($activity) {
$query = new ActivityQuery(); $query = new ActivityFormTypeQuery();
$query $query->setProject($activity->getProject());
->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER) $query->setActivityToIgnore($activity);
->setProject($activity->getProject())
->setOrderGlobalsFirst(true)
->addIgnoredEntity($activity)
->setGlobalsOnly(null === $activity->getProject())
;
return $repo->findByQuery($query); return $repo->getQueryBuilderForFormType($query);
}, },
'required' => false, 'required' => false,
]) ])

View File

@@ -16,9 +16,9 @@ use App\Form\CustomerEditForm;
use App\Form\Toolbar\CustomerToolbarForm; use App\Form\Toolbar\CustomerToolbarForm;
use App\Form\Type\CustomerType; use App\Form\Type\CustomerType;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\CustomerQuery; use App\Repository\Query\CustomerQuery;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -79,25 +79,18 @@ class CustomerController extends AbstractController
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
$query = new CustomerQuery(); $query = new CustomerQuery();
$query $query->setPage($page);
->setOrderBy('name')
->setPage($page)
;
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var CustomerQuery $query */
$query = $form->getData();
}
/* @var $entries Pagerfanta */ $entries = $this->getRepository()->getPagerfantaForQuery($query);
$entries = $this->getRepository()->findByQuery($query);
return $this->render('customer/index.html.twig', [ return $this->render('customer/index.html.twig', [
'entries' => $entries, 'entries' => $entries,
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $query->isDirty(),
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
]); ]);
} }
@@ -174,12 +167,10 @@ class CustomerController extends AbstractController
->add('customer', CustomerType::class, [ ->add('customer', CustomerType::class, [
'label' => 'label.customer', 'label' => 'label.customer',
'query_builder' => function (CustomerRepository $repo) use ($customer) { 'query_builder' => function (CustomerRepository $repo) use ($customer) {
$query = new CustomerQuery(); $query = new CustomerFormTypeQuery();
$query $query->setCustomerToIgnore($customer);
->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER)
->addIgnoredEntity($customer);
return $repo->findByQuery($query); return $repo->getQueryBuilderForFormType($query);
}, },
'required' => false, 'required' => false,
]) ])

View File

@@ -74,7 +74,7 @@ class ExportController extends AbstractController
} }
/** /**
* @Route(path="/", name="export", methods={"GET", "POST"}) * @Route(path="/", name="export", methods={"GET"})
* @Security("is_granted('view_export')") * @Security("is_granted('view_export')")
* *
* @param Request $request * @param Request $request
@@ -84,13 +84,10 @@ class ExportController extends AbstractController
public function indexAction(Request $request) public function indexAction(Request $request)
{ {
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { $form = $this->getToolbarForm($query, 'GET');
/** @var ExportQuery $query */ $form->setData($query);
$query = $form->getData(); $form->submit($request->query->all(), false);
}
$entries = $this->getEntries($query); $entries = $this->getEntries($query);
@@ -103,7 +100,7 @@ class ExportController extends AbstractController
} }
/** /**
* @Route(path="/data", name="export_data", methods={"GET", "POST"}) * @Route(path="/data", name="export_data", methods={"POST"})
* @Security("is_granted('create_export')") * @Security("is_granted('create_export')")
* *
* @param Request $request * @param Request $request
@@ -113,13 +110,9 @@ class ExportController extends AbstractController
public function export(Request $request) public function export(Request $request)
{ {
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { $form = $this->getToolbarForm($query, 'POST');
/** @var ExportQuery $query */ $form->handleRequest($request);
$query = $form->getData();
}
$type = $query->getType(); $type = $query->getType();
if (null === $type) { if (null === $type) {
@@ -149,15 +142,11 @@ class ExportController extends AbstractController
return $this->timesheetRepository->getTimesheetsForQuery($query); return $this->timesheetRepository->getTimesheetsForQuery($query);
} }
/** protected function getToolbarForm(ExportQuery $query, string $method): FormInterface
* @param ExportQuery $query
* @return FormInterface
*/
protected function getToolbarForm(ExportQuery $query): FormInterface
{ {
return $this->createForm(ExportToolbarForm::class, $query, [ return $this->createForm(ExportToolbarForm::class, $query, [
'action' => $this->generateUrl('export', []), 'action' => $this->generateUrl('export', []),
'method' => 'POST', 'method' => $method,
'attr' => [ 'attr' => [
'id' => 'export-form' 'id' => 'export-form'
] ]

View File

@@ -21,6 +21,7 @@ use App\Repository\Query\InvoiceQuery;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory; use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -75,7 +76,7 @@ class InvoiceController extends AbstractController
} }
/** /**
* @Route(path="/", name="invoice", methods={"GET", "POST"}) * @Route(path="/", name="invoice", methods={"GET"})
* @Security("is_granted('view_invoice')") * @Security("is_granted('view_invoice')")
* *
* @param Request $request * @param Request $request
@@ -91,12 +92,11 @@ class InvoiceController extends AbstractController
$entries = []; $entries = [];
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query, 'GET');
$form->handleRequest($request); $form->setData($query);
$form->submit($request->query->all(), false);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
/** @var InvoiceQuery $query */
$query = $form->getData();
$entries = $this->getEntries($query, $repository); $entries = $this->getEntries($query, $repository);
} }
@@ -109,7 +109,7 @@ class InvoiceController extends AbstractController
} }
/** /**
* @Route(path="/print", name="invoice_print", methods={"GET", "POST"}) * @Route(path="/print", name="invoice_print", methods={"POST"})
* @Security("is_granted('create_invoice')") * @Security("is_granted('create_invoice')")
* *
* @param Request $request * @param Request $request
@@ -123,7 +123,8 @@ class InvoiceController extends AbstractController
} }
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query, 'POST');
$form->handleRequest($request); $form->handleRequest($request);
if (!$form->isSubmitted() || !$form->isValid()) { if (!$form->isSubmitted() || !$form->isValid()) {
@@ -328,15 +329,11 @@ class InvoiceController extends AbstractController
]); ]);
} }
/** protected function getToolbarForm(InvoiceQuery $query, string $method): FormInterface
* @param InvoiceQuery $query
* @return \Symfony\Component\Form\FormInterface
*/
protected function getToolbarForm(InvoiceQuery $query)
{ {
return $this->createForm(InvoiceToolbarForm::class, $query, [ return $this->createForm(InvoiceToolbarForm::class, $query, [
'action' => $this->generateUrl('invoice', []), 'action' => $this->generateUrl('invoice', []),
'method' => 'POST', 'method' => $method,
'attr' => [ 'attr' => [
'id' => 'invoice-print-form' 'id' => 'invoice-print-form'
], ],

View File

@@ -16,6 +16,7 @@ use App\Form\ProjectEditForm;
use App\Form\Toolbar\ProjectToolbarForm; use App\Form\Toolbar\ProjectToolbarForm;
use App\Form\Type\ProjectType; use App\Form\Type\ProjectType;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Repository\Query\ProjectQuery; use App\Repository\Query\ProjectQuery;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
@@ -67,26 +68,19 @@ class ProjectController extends AbstractController
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
$query = new ProjectQuery(); $query = new ProjectQuery();
$query $query->setPage($page);
->setOrderBy('name')
->setExclusiveVisibility(true)
->setPage($page)
;
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var ProjectQuery $query */
$query = $form->getData();
}
/* @var $entries Pagerfanta */ /* @var $entries Pagerfanta */
$entries = $this->getRepository()->findByQuery($query); $entries = $this->getRepository()->getPagerfantaForQuery($query);
return $this->render('project/index.html.twig', [ return $this->render('project/index.html.twig', [
'entries' => $entries, 'entries' => $entries,
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $query->isDirty(),
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
]); ]);
} }
@@ -161,13 +155,11 @@ class ProjectController extends AbstractController
->add('project', ProjectType::class, [ ->add('project', ProjectType::class, [
'label' => 'label.project', 'label' => 'label.project',
'query_builder' => function (ProjectRepository $repo) use ($project) { 'query_builder' => function (ProjectRepository $repo) use ($project) {
$query = new ProjectQuery(); $query = new ProjectFormTypeQuery();
$query $query->setCustomer($project->getCustomer());
->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER) $query->setProjectToIgnore($project);
->setCustomer($project->getCustomer())
->addIgnoredEntity($project);
return $repo->findByQuery($query); return $repo->getQueryBuilderForFormType($query);
}, },
'required' => false, 'required' => false,
]) ])

View File

@@ -38,18 +38,15 @@ class TagController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var TagQuery $query */
$query = $form->getData();
}
$tags = $repository->getTagCount($query); $tags = $repository->getTagCount($query);
return $this->render('tags/index.html.twig', [ return $this->render('tags/index.html.twig', [
'tags' => $tags, 'tags' => $tags,
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $query->isDirty(),
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
]); ]);
} }

View File

@@ -87,10 +87,10 @@ abstract class TimesheetAbstractController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var TimesheetQuery $query */
$query = $form->getData(); if ($form->isValid()) {
if (null !== $query->getBegin()) { if (null !== $query->getBegin()) {
$query->getBegin()->setTime(0, 0, 0); $query->getBegin()->setTime(0, 0, 0);
} }
@@ -99,10 +99,6 @@ abstract class TimesheetAbstractController extends AbstractController
} }
} }
if (!$this->includeUserInForms()) {
$query->setUser($this->getUser());
}
$tags = $query->getTags(true); $tags = $query->getTags(true);
if (!empty($tags)) { if (!empty($tags)) {
/** @var TagRepository $tagRepo */ /** @var TagRepository $tagRepo */
@@ -114,13 +110,19 @@ abstract class TimesheetAbstractController extends AbstractController
); );
} }
$dirtyQuery = $query->isDirty();
if (!$this->includeUserInForms()) {
$query->setUser($this->getUser());
}
$pager = $this->getRepository()->getPagerfantaForQuery($query); $pager = $this->getRepository()->getPagerfantaForQuery($query);
return $this->render($renderTemplate, [ return $this->render($renderTemplate, [
'entries' => $pager, 'entries' => $pager,
'page' => $query->getPage(), 'page' => $query->getPage(),
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $dirtyQuery,
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
'showSummary' => $this->includeSummary(), 'showSummary' => $this->includeSummary(),
'showStartEndTime' => $this->canSeeStartEndTime() 'showStartEndTime' => $this->canSeeStartEndTime()
@@ -235,11 +237,8 @@ abstract class TimesheetAbstractController extends AbstractController
$query = new TimesheetQuery(); $query = new TimesheetQuery();
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var TimesheetQuery $query */
$query = $form->getData();
}
// by default the current month is exported, but it can be overwritten // by default the current month is exported, but it can be overwritten
// this should not be removed, otherwise we would export EVERY available record in the admin section // this should not be removed, otherwise we would export EVERY available record in the admin section

View File

@@ -69,11 +69,8 @@ class UserController extends AbstractController
$query->setOrderBy('username'); $query->setOrderBy('username');
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->handleRequest($request); $form->setData($query);
if ($form->isSubmitted() && $form->isValid()) { $form->submit($request->query->all(), false);
/** @var UserQuery $query */
$query = $form->getData();
}
/* @var $entries Pagerfanta */ /* @var $entries Pagerfanta */
$entries = $this->getRepository()->findByQuery($query); $entries = $this->getRepository()->findByQuery($query);
@@ -81,7 +78,7 @@ class UserController extends AbstractController
return $this->render('user/index.html.twig', [ return $this->render('user/index.html.twig', [
'entries' => $entries, 'entries' => $entries,
'query' => $query, 'query' => $query,
'showFilter' => $form->isSubmitted(), 'showFilter' => $query->isDirty(),
'toolbarForm' => $form->createView(), 'toolbarForm' => $form->createView(),
]); ]);
} }

View File

@@ -15,6 +15,8 @@ use App\Form\Type\CustomerType;
use App\Form\Type\ProjectType; use App\Form\Type\ProjectType;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -66,7 +68,7 @@ class ActivityEditForm extends AbstractType
$builder $builder
->add('customer', CustomerType::class, [ ->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($customer) { 'query_builder' => function (CustomerRepository $repo) use ($customer) {
return $repo->builderForEntityType($customer); return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
}, },
'data' => $customer ? $customer : null, 'data' => $customer ? $customer : null,
'required' => false, 'required' => false,
@@ -79,7 +81,7 @@ class ActivityEditForm extends AbstractType
->add('project', ProjectType::class, [ ->add('project', ProjectType::class, [
'required' => false, 'required' => false,
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) { 'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
return $repo->builderForEntityType($project, $customer); return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
}, },
]); ]);
@@ -95,7 +97,7 @@ class ActivityEditForm extends AbstractType
$event->getForm()->add('project', ProjectType::class, [ $event->getForm()->add('project', ProjectType::class, [
'group_by' => null, 'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($data, $project) { 'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
return $repo->builderForEntityType($project, $data['customer']); return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $data['customer']));
}, },
]); ]);
} }

View File

@@ -13,6 +13,7 @@ use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Form\Type\CustomerType; use App\Form\Type\CustomerType;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -59,7 +60,7 @@ class ProjectEditForm extends AbstractType
]) ])
->add('customer', CustomerType::class, [ ->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($customer) { 'query_builder' => function (CustomerRepository $repo) use ($customer) {
return $repo->builderForEntityType($customer); return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
}, },
]); ]);

View File

@@ -27,6 +27,9 @@ use App\Form\Type\YesNoType;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Timesheet\UserDateTimeFactory; use App\Timesheet\UserDateTimeFactory;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@@ -162,11 +165,11 @@ class TimesheetEditForm extends AbstractType
$builder $builder
->add('customer', CustomerType::class, [ ->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($customer) { 'query_builder' => function (CustomerRepository $repo) use ($customer) {
return $repo->builderForEntityType($customer); return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
}, },
'data' => $customer ? $customer : '', 'data' => $customer ? $customer : '',
'required' => false, 'required' => false,
'placeholder' => null === $customer ? '' : null, 'placeholder' => '',
'mapped' => false, 'mapped' => false,
'project_enabled' => true, 'project_enabled' => true,
]); ]);
@@ -192,7 +195,7 @@ class TimesheetEditForm extends AbstractType
'placeholder' => '', 'placeholder' => '',
'activity_enabled' => true, 'activity_enabled' => true,
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) { 'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
return $repo->builderForEntityType($project, $customer); return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
}, },
]) ])
); );
@@ -211,7 +214,7 @@ class TimesheetEditForm extends AbstractType
'activity_enabled' => true, 'activity_enabled' => true,
'group_by' => null, 'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($data, $project) { 'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
return $repo->builderForEntityType($project, $data['customer']); return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $data['customer']));
}, },
]); ]);
} }
@@ -224,7 +227,7 @@ class TimesheetEditForm extends AbstractType
->add('activity', ActivityType::class, [ ->add('activity', ActivityType::class, [
'placeholder' => '', 'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($activity, $project) { 'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
return $repo->builderForEntityType($activity, $project); return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $project));
}, },
]) ])
; ;
@@ -241,7 +244,7 @@ class TimesheetEditForm extends AbstractType
$event->getForm()->add('activity', ActivityType::class, [ $event->getForm()->add('activity', ActivityType::class, [
'placeholder' => '', 'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($data, $activity) { 'query_builder' => function (ActivityRepository $repo) use ($data, $activity) {
return $repo->builderForEntityType($activity, $data['project']); return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $data['project']));
}, },
]); ]);
} }

View File

@@ -21,9 +21,9 @@ use App\Form\Type\VisibilityType;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityQuery; use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\CustomerQuery; use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectQuery; use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -39,7 +39,7 @@ abstract class AbstractToolbarForm extends AbstractType
{ {
/** /**
* Dirty hack to enable easy handling of GET form in controller and javascript. * Dirty hack to enable easy handling of GET form in controller and javascript.
*Cleans up the name of all form elents (and unfortunately of the form itself). * Cleans up the name of all form elements (and unfortunately of the form itself).
* *
* @return null|string * @return null|string
*/ */
@@ -59,22 +59,29 @@ abstract class AbstractToolbarForm extends AbstractType
]); ]);
} }
/**
* @param FormBuilderInterface $builder
*/
protected function addCustomerChoice(FormBuilderInterface $builder) protected function addCustomerChoice(FormBuilderInterface $builder)
{ {
$builder->add('customer', CustomerType::class, [ // just a fake field for having this field at the right position in the frontend
'required' => false, $builder->add('customer', HiddenType::class);
'project_enabled' => true,
'query_builder' => function (CustomerRepository $repo) {
$query = new CustomerQuery();
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
$query->setOrderBy('name');
return $repo->findByQuery($query); $builder->addEventListener(
}, FormEvents::PRE_SUBMIT,
]); function (FormEvent $event) {
$data = $event->getData();
$event->getForm()->add('customer', CustomerType::class, [
'required' => false,
'project_enabled' => true,
'query_builder' => function (CustomerRepository $repo) use ($data) {
$query = new CustomerFormTypeQuery();
if (isset($data['customer']) && !empty($data['customer'])) {
$query->setCustomer($data['customer']);
}
return $repo->getQueryBuilderForFormType($query);
},
]);
}
);
} }
/** /**
@@ -126,32 +133,27 @@ abstract class AbstractToolbarForm extends AbstractType
*/ */
protected function addProjectChoice(FormBuilderInterface $builder) protected function addProjectChoice(FormBuilderInterface $builder)
{ {
$builder->add('project', ProjectType::class, [ // just a fake field for having this field at the right position in the frontend
'required' => false, $builder->add('project', HiddenType::class);
'activity_enabled' => true,
'choices' => [],
'disabled' => true,
]);
$builder->addEventListener( $builder->addEventListener(
FormEvents::PRE_SUBMIT, FormEvents::PRE_SUBMIT,
function (FormEvent $event) { function (FormEvent $event) {
$data = $event->getData(); $data = $event->getData();
if (!isset($data['customer']) || empty($data['customer'])) {
return;
}
$event->getForm()->add('project', ProjectType::class, [ $event->getForm()->add('project', ProjectType::class, [
'group_by' => null,
'required' => false, 'required' => false,
'activity_enabled' => true, 'activity_enabled' => true,
'query_builder' => function (ProjectRepository $repo) use ($data) { 'query_builder' => function (ProjectRepository $repo) use ($data) {
$query = new ProjectQuery(); $query = new ProjectFormTypeQuery();
$query->setCustomer($data['customer']);
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
$query->setOrderBy('name');
return $repo->findByQuery($query); if (isset($data['customer']) && !empty($data['customer'])) {
$query->setCustomer($data['customer']);
}
if (isset($data['project']) && !empty($data['project'])) {
$query->setProject($data['project']);
}
return $repo->getQueryBuilderForFormType($query);
}, },
]); ]);
} }
@@ -163,37 +165,26 @@ abstract class AbstractToolbarForm extends AbstractType
*/ */
protected function addActivityChoice(FormBuilderInterface $builder) protected function addActivityChoice(FormBuilderInterface $builder)
{ {
$builder->add('activity', ActivityType::class, [ // just a fake field for having this field at the right position in the frontend
'required' => false, $builder->add('activity', HiddenType::class);
'query_builder' => function (ActivityRepository $repo) {
$query = new ActivityQuery();
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
$query->setGlobalsOnly(true);
$query->setOrderGlobalsFirst(true);
$query->setOrderBy('name');
return $repo->findByQuery($query);
},
]);
$builder->addEventListener( $builder->addEventListener(
FormEvents::PRE_SUBMIT, FormEvents::PRE_SUBMIT,
function (FormEvent $event) { function (FormEvent $event) {
$data = $event->getData(); $data = $event->getData();
if (!isset($data['project']) || empty($data['project'])) {
return;
}
$event->getForm()->add('activity', ActivityType::class, [ $event->getForm()->add('activity', ActivityType::class, [
'required' => false, 'required' => false,
'query_builder' => function (ActivityRepository $repo) use ($data) { 'query_builder' => function (ActivityRepository $repo) use ($data) {
$query = new ActivityQuery(); $query = new ActivityFormTypeQuery();
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
$query->setProject($data['project']);
$query->setOrderGlobalsFirst(true);
$query->setOrderBy('name');
return $repo->findByQuery($query); if (isset($data['activity']) && !empty($data['activity'])) {
$query->setActivity($data['activity']);
}
if (isset($data['project']) && !empty($data['project'])) {
$query->setProject($data['project']);
}
return $repo->getQueryBuilderForFormType($query);
}, },
]); ]);
} }

View File

@@ -11,6 +11,7 @@ namespace App\Form\Type;
use App\Entity\Activity; use App\Entity\Activity;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -78,7 +79,7 @@ class ActivityType extends AbstractType
'group_by' => [$this, 'groupBy'], 'group_by' => [$this, 'groupBy'],
'choice_attr' => [$this, 'choiceAttr'], 'choice_attr' => [$this, 'choiceAttr'],
'query_builder' => function (ActivityRepository $repo) { 'query_builder' => function (ActivityRepository $repo) {
return $repo->builderForEntityType(); return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery());
}, },
]); ]);
} }

View File

@@ -11,6 +11,7 @@ namespace App\Form\Type;
use App\Entity\Customer; use App\Entity\Customer;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectQuery; use App\Repository\Query\ProjectQuery;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@@ -37,7 +38,7 @@ class CustomerType extends AbstractType
'class' => Customer::class, 'class' => Customer::class,
'choice_label' => 'name', 'choice_label' => 'name',
'query_builder' => function (CustomerRepository $repo) { 'query_builder' => function (CustomerRepository $repo) {
return $repo->builderForEntityType(null); return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery());
}, },
'project_enabled' => false, 'project_enabled' => false,
'project_visibility' => ProjectQuery::SHOW_VISIBLE, 'project_visibility' => ProjectQuery::SHOW_VISIBLE,
@@ -48,7 +49,8 @@ class CustomerType extends AbstractType
return [ return [
'select' => 'project', 'select' => 'project',
'route' => 'get_projects', 'route' => 'get_projects',
'route_params' => ['customer' => '-s-', 'orderBy' => 'name', 'visible' => $options['project_visibility']], 'route_params' => ['customer' => '-s-', 'visible' => $options['project_visibility']],
'empty_route_params' => ['visible' => $options['project_visibility']],
]; ];
} }

View File

@@ -12,6 +12,7 @@ namespace App\Form\Type;
use App\Entity\Project; use App\Entity\Project;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityQuery; use App\Repository\Query\ActivityQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
@@ -62,7 +63,7 @@ class ProjectType extends AbstractType
return $project->getCustomer()->getName(); return $project->getCustomer()->getName();
}, },
'query_builder' => function (ProjectRepository $repo) { 'query_builder' => function (ProjectRepository $repo) {
return $repo->builderForEntityType(null); return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery());
}, },
'activity_enabled' => false, 'activity_enabled' => false,
'activity_visibility' => ActivityQuery::SHOW_VISIBLE, 'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
@@ -73,8 +74,8 @@ class ProjectType extends AbstractType
return [ return [
'select' => 'activity', 'select' => 'activity',
'route' => 'get_activities', 'route' => 'get_activities',
'route_params' => ['project' => '-s-', 'orderBy' => 'name', 'visible' => $options['activity_visibility']], 'route_params' => ['project' => '-s-', 'visible' => $options['activity_visibility']],
'empty_route_params' => ['globals' => 'true', 'orderBy' => 'name', 'visible' => $options['activity_visibility']], 'empty_route_params' => ['globals' => 'true', 'visible' => $options['activity_visibility']],
]; ];
} }

View File

@@ -1,20 +0,0 @@
<?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\Repository;
use Doctrine\ORM\EntityRepository;
/**
* Class AbstractRepository
*/
abstract class AbstractRepository extends EntityRepository
{
use RepositoryTrait;
}

View File

@@ -10,16 +10,20 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Model\ActivityStatistic; use App\Model\ActivityStatistic;
use App\Repository\Loader\ActivityLoader;
use App\Repository\Paginator\LoaderPaginator;
use App\Repository\Paginator\PaginatorInterface;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\ActivityQuery; use App\Repository\Query\ActivityQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
class ActivityRepository extends AbstractRepository class ActivityRepository extends EntityRepository
{ {
/** /**
* @param Activity $activity * @param Activity $activity
@@ -33,15 +37,6 @@ class ActivityRepository extends AbstractRepository
$entityManager->flush(); $entityManager->flush();
} }
/**
* @param int $id
* @return null|Activity
*/
public function getById($id)
{
return $this->find($id);
}
/** /**
* @param null|bool $visible * @param null|bool $visible
* @return int * @return int
@@ -89,67 +84,128 @@ class ActivityRepository extends AbstractRepository
/** /**
* Returns a query builder that is used for ActivityType and your own 'query_builder' option. * Returns a query builder that is used for ActivityType and your own 'query_builder' option.
* *
* @param Activity|string|null $activity * @param ActivityFormTypeQuery $query
* @param Project|string|null $project * @return QueryBuilder
* @return \Doctrine\ORM\QueryBuilder
*/ */
public function builderForEntityType($activity = null, $project = null) public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder
{
$query = new ActivityQuery();
$query->setHiddenEntity($activity);
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
$query->setProject($project);
$query->setOrderGlobalsFirst(true);
$query->setOrderBy('name');
if (null === $activity && $project === null) {
$query->setGlobalsOnly(true);
}
return $this->findByQuery($query);
}
/**
* @param ActivityQuery $query
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(ActivityQuery $query)
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('a', 'p', 'c') $qb->select('a')
->from(Activity::class, 'a') ->from(Activity::class, 'a')
->leftJoin('a.project', 'p') ->addOrderBy('a.project', 'DESC')
->leftJoin('p.customer', 'c'); ->addOrderBy('a.name', 'ASC')
;
if ($query->isOrderGlobalsFirst()) {
$qb->orderBy('a.project', 'ASC');
}
$qb->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder());
$where = $qb->expr()->andX(); $where = $qb->expr()->andX();
if (ActivityQuery::SHOW_VISIBLE == $query->getVisibility()) { $where->add('a.visible = :visible');
$where->add('a.visible = :visible'); $qb->setParameter('visible', true, \PDO::PARAM_BOOL);
if (!$query->isExclusiveVisibility()) {
if (!$query->isGlobalsOnly()) {
$qb
->addSelect('p')
->addSelect('c')
->leftJoin('a.project', 'p')
->leftJoin('p.customer', 'c');
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('c.visible', ':customer_visible'),
$qb->expr()->isNull('c.visible')
)
);
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('p.visible', ':project_visible'),
$qb->expr()->isNull('p.visible')
)
);
$qb->setParameter('project_visible', true, \PDO::PARAM_BOOL);
$qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
}
if ($query->isGlobalsOnly()) {
$where->add($qb->expr()->isNull('a.project'));
} elseif (null !== $query->getProject()) {
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('a.project', ':project'),
$qb->expr()->isNull('a.project')
)
);
$qb->setParameter('project', $query->getProject());
}
if (null !== $query->getActivityToIgnore()) {
$qb->andWhere($qb->expr()->neq('a.id', ':ignored'));
$qb->setParameter('ignored', $query->getActivityToIgnore());
}
$or = $qb->expr()->orX();
// this must always be the last part before the or
$or->add($where);
// this must always be the last part of the query
/* @var Activity $entity */
if (null !== $query->getActivity()) {
$or->add($qb->expr()->eq('a.id', ':activity'));
$qb->setParameter('activity', $query->getActivity());
}
if ($or->count() > 0) {
$qb->andWhere($or);
}
return $qb;
}
private function getQueryBuilderForQuery(ActivityQuery $query): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('a')
->from(Activity::class, 'a')
->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder())
;
if (!$query->isGlobalsOnly()) {
$qb
->leftJoin('a.project', 'p')
->leftJoin('p.customer', 'c')
;
}
$where = $qb->expr()->andX();
if (in_array($query->getVisibility(), [ActivityQuery::SHOW_VISIBLE, ActivityQuery::SHOW_HIDDEN])) {
if (!$query->isGlobalsOnly()) {
$where->add( $where->add(
$qb->expr()->orX( $qb->expr()->orX(
$qb->expr()->eq('c.visible', ':visible'), $qb->expr()->eq('c.visible', ':customer_visible'),
$qb->expr()->isNull('c.visible') $qb->expr()->isNull('c.visible')
) )
); );
$where->add( $where->add(
$qb->expr()->orX( $qb->expr()->orX(
$qb->expr()->eq('p.visible', ':visible'), $qb->expr()->eq('p.visible', ':project_visible'),
$qb->expr()->isNull('p.visible') $qb->expr()->isNull('p.visible')
) )
); );
$qb->setParameter('project_visible', true, \PDO::PARAM_BOOL);
$qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
} }
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
} elseif (ActivityQuery::SHOW_HIDDEN == $query->getVisibility()) {
$where->add('a.visible = :visible'); $where->add('a.visible = :visible');
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
if (ActivityQuery::SHOW_VISIBLE === $query->getVisibility()) {
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
} elseif (ActivityQuery::SHOW_HIDDEN === $query->getVisibility()) {
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
}
} }
if ($query->isGlobalsOnly()) { if ($query->isGlobalsOnly()) {
@@ -167,29 +223,48 @@ class ActivityRepository extends AbstractRepository
$qb->setParameter('customer', $query->getCustomer()); $qb->setParameter('customer', $query->getCustomer());
} }
if (!empty($query->getIgnoredEntities())) { if ($where->count() > 0) {
$qb->andWhere('a.id NOT IN(:ignored)'); $qb->andWhere($where);
$qb->setParameter('ignored', $query->getIgnoredEntities());
} }
$or = $qb->expr()->orX(); return $qb;
}
// this must always be the last part before the or public function getPagerfantaForQuery(ActivityQuery $query): Pagerfanta
$or->add($where); {
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
$paginator->setMaxPerPage($query->getPageSize());
$paginator->setCurrentPage($query->getPage());
// this must always be the last part of the query return $paginator;
/** @var Activity $entity */ }
$entity = $query->getHiddenEntity();
if (null !== $entity) {
$or->add($qb->expr()->eq('a.id', ':activity'));
$qb->setParameter('activity', $entity);
}
if ($or->count() > 0) { protected function getPaginatorForQuery(ActivityQuery $query): PaginatorInterface
$qb->andWhere($or); {
} $qb = $this->getQueryBuilderForQuery($query);
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->countDistinct('a.id'))
;
$counter = (int) $qb->getQuery()->getSingleScalarResult();
return $this->getBaseQueryResult($qb, $query); $qb = $this->getQueryBuilderForQuery($query);
return new LoaderPaginator(new ActivityLoader($qb->getEntityManager()), $qb, $counter);
}
/**
* @param ActivityQuery $query
* @return Activity[]
*/
public function getActivitiesForQuery(ActivityQuery $query): iterable
{
// this is using the paginator internally, as it will load all joined entities into the working unit
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
$paginator = $this->getPaginatorForQuery($query);
return $paginator->getAll();
} }
/** /**

View File

@@ -12,10 +12,11 @@ namespace App\Repository;
use App\Configuration\ConfigLoaderInterface; use App\Configuration\ConfigLoaderInterface;
use App\Entity\Configuration; use App\Entity\Configuration;
use App\Form\Model\SystemConfiguration; use App\Form\Model\SystemConfiguration;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
class ConfigurationRepository extends AbstractRepository implements ConfigLoaderInterface class ConfigurationRepository extends EntityRepository implements ConfigLoaderInterface
{ {
/** /**
* @param string $prefix * @param string $prefix

View File

@@ -14,13 +14,18 @@ use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Model\CustomerStatistic; use App\Model\CustomerStatistic;
use App\Repository\Loader\CustomerLoader;
use App\Repository\Paginator\LoaderPaginator;
use App\Repository\Paginator\PaginatorInterface;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\CustomerQuery; use App\Repository\Query\CustomerQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
class CustomerRepository extends AbstractRepository class CustomerRepository extends EntityRepository
{ {
/** /**
* @param Customer $customer * @param Customer $customer
@@ -34,15 +39,6 @@ class CustomerRepository extends AbstractRepository
$entityManager->flush(); $entityManager->flush();
} }
/**
* @param int $id
* @return null|Customer
*/
public function getById($id)
{
return $this->find($id);
}
/** /**
* @param null|bool $visible * @param null|bool $visible
* @return int * @return int
@@ -114,51 +110,88 @@ class CustomerRepository extends AbstractRepository
/** /**
* Returns a query builder that is used for CustomerType and your own 'query_builder' option. * Returns a query builder that is used for CustomerType and your own 'query_builder' option.
* *
* @param Customer|null $entity * @param CustomerFormTypeQuery $query
* @return \Doctrine\ORM\QueryBuilder * @return QueryBuilder
*/ */
public function builderForEntityType(Customer $entity = null) public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
{
$query = new CustomerQuery();
$query->setHiddenEntity($entity);
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
$query->setOrderBy('name');
return $this->findByQuery($query);
}
/**
* @param CustomerQuery $query
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(CustomerQuery $query)
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('c') $qb->select('c')
->from(Customer::class, 'c') ->from(Customer::class, 'c')
->orderBy('c.name', 'ASC');
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
$customer = $query->getCustomer();
if (null !== $customer) {
$qb->orWhere('c.id = :customer')->setParameter('customer', $customer);
}
if (null !== $query->getCustomerToIgnore()) {
$qb->andWhere($qb->expr()->neq('c.id', ':ignored'));
$qb->setParameter('ignored', $query->getCustomerToIgnore());
}
return $qb;
}
private function getQueryBuilderForQuery(CustomerQuery $query): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('c', 'meta')
->from(Customer::class, 'c')
->leftJoin('c.meta', 'meta')
->orderBy('c.' . $query->getOrderBy(), $query->getOrder()); ->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
if (CustomerQuery::SHOW_VISIBLE == $query->getVisibility()) { if (CustomerQuery::SHOW_VISIBLE == $query->getVisibility()) {
$qb->andWhere($qb->expr()->eq('c.visible', ':visible')); $qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
$qb->setParameter('visible', true, \PDO::PARAM_BOOL); $qb->setParameter('visible', true, \PDO::PARAM_BOOL);
/** @var Customer $entity */
$entity = $query->getHiddenEntity();
if (null !== $entity) {
$qb->orWhere('c.id = :customer')->setParameter('customer', $entity);
}
} elseif (CustomerQuery::SHOW_HIDDEN == $query->getVisibility()) { } elseif (CustomerQuery::SHOW_HIDDEN == $query->getVisibility()) {
$qb->andWhere($qb->expr()->eq('c.visible', ':visible')); $qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
$qb->setParameter('visible', false, \PDO::PARAM_BOOL); $qb->setParameter('visible', false, \PDO::PARAM_BOOL);
} }
if (!empty($query->getIgnoredEntities())) { return $qb;
$qb->andWhere('c.id NOT IN(:ignored)'); }
$qb->setParameter('ignored', $query->getIgnoredEntities());
}
return $this->getBaseQueryResult($qb, $query); public function getPagerfantaForQuery(CustomerQuery $query): Pagerfanta
{
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
$paginator->setMaxPerPage($query->getPageSize());
$paginator->setCurrentPage($query->getPage());
return $paginator;
}
protected function getPaginatorForQuery(CustomerQuery $query): PaginatorInterface
{
$qb = $this->getQueryBuilderForQuery($query);
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->countDistinct('c.id'))
;
$counter = (int) $qb->getQuery()->getSingleScalarResult();
$qb = $this->getQueryBuilderForQuery($query);
return new LoaderPaginator(new CustomerLoader($qb->getEntityManager()), $qb, $counter);
}
/**
* @param CustomerQuery $query
* @return Customer[]
*/
public function getCustomersForQuery(CustomerQuery $query): iterable
{
// this is using the paginator internally, as it will load all joined entities into the working unit
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
$paginator = $this->getPaginatorForQuery($query);
return $paginator->getAll();
} }
/** /**

View File

@@ -11,15 +11,15 @@ namespace App\Repository;
use App\Entity\InvoiceTemplate; use App\Entity\InvoiceTemplate;
use App\Repository\Query\BaseQuery; use App\Repository\Query\BaseQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
/** class InvoiceTemplateRepository extends EntityRepository
* Class InvoiceTemplateRepository
*/
class InvoiceTemplateRepository extends AbstractRepository
{ {
use RepositoryTrait;
/** /**
* @return bool * @return bool
*/ */
@@ -50,7 +50,7 @@ class InvoiceTemplateRepository extends AbstractRepository
$qb->select('t') $qb->select('t')
->from(InvoiceTemplate::class, 't') ->from(InvoiceTemplate::class, 't')
->orderBy('t.id'); ->orderBy('t.name');
return $this->getBaseQueryResult($qb, $query); return $this->getBaseQueryResult($qb, $query);
} }

View File

@@ -0,0 +1,74 @@
<?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\Repository\Loader;
use App\Entity\Activity;
use App\Entity\Project;
use Doctrine\ORM\EntityManagerInterface;
final class ActivityIdLoader implements LoaderInterface
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @param int[] $ids
*/
public function loadResults(array $ids): void
{
if (empty($ids)) {
return;
}
$em = $this->entityManager;
$qb = $em->createQueryBuilder();
$activities = $qb->select('PARTIAL a.{id}', 'project')
->from(Activity::class, 'a')
->leftJoin('a.project', 'project')
->andWhere($qb->expr()->isNotNull('a.project'))
->andWhere($qb->expr()->in('a.id', $ids))
->getQuery()
->execute();
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL a.{id}', 'meta')
->from(Activity::class, 'a')
->leftJoin('a.meta', 'meta')
->andWhere($qb->expr()->in('a.id', $ids))
->getQuery()
->execute();
if (!empty($activities)) {
$projectIds = array_map(function (Activity $activity) {
if (null === $activity->getProject()) {
return null;
}
return $activity->getProject()->getId();
}, $activities);
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL p.{id}', 'customer')
->from(Project::class, 'p')
->leftJoin('p.customer', 'customer')
->andWhere($qb->expr()->in('p.id', $projectIds))
->getQuery()
->execute();
}
}
}

View File

@@ -0,0 +1,38 @@
<?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\Repository\Loader;
use App\Entity\Activity;
use Doctrine\ORM\EntityManagerInterface;
final class ActivityLoader implements LoaderInterface
{
/**
* @var ActivityIdLoader
*/
private $loader;
public function __construct(EntityManagerInterface $entityManager)
{
$this->loader = new ActivityIdLoader($entityManager);
}
/**
* @param Activity[] $activities
*/
public function loadResults(array $activities): void
{
$ids = array_map(function (Activity $activity) {
return $activity->getId();
}, $activities);
$this->loader->loadResults($ids);
}
}

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\Repository\Loader;
use App\Entity\Customer;
use Doctrine\ORM\EntityManagerInterface;
final class CustomerIdLoader implements LoaderInterface
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @param int[] $ids
*/
public function loadResults(array $ids): void
{
if (empty($ids)) {
return;
}
$em = $this->entityManager;
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL c.{id}', 'meta')
->from(Customer::class, 'c')
->leftJoin('c.meta', 'meta')
->andWhere($qb->expr()->in('c.id', $ids))
->getQuery()
->execute();
}
}

View File

@@ -0,0 +1,38 @@
<?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\Repository\Loader;
use App\Entity\Customer;
use Doctrine\ORM\EntityManagerInterface;
final class CustomerLoader implements LoaderInterface
{
/**
* @var CustomerIdLoader
*/
private $loader;
public function __construct(EntityManagerInterface $entityManager)
{
$this->loader = new CustomerIdLoader($entityManager);
}
/**
* @param Customer[] $customers
*/
public function loadResults(array $customers): void
{
$ids = array_map(function (Customer $customer) {
return $customer->getId();
}, $customers);
$this->loader->loadResults($ids);
}
}

View File

@@ -0,0 +1,54 @@
<?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\Repository\Loader;
use App\Entity\Project;
use Doctrine\ORM\EntityManagerInterface;
final class ProjectIdLoader implements LoaderInterface
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @param int[] $ids
*/
public function loadResults(array $ids): void
{
if (empty($ids)) {
return;
}
$em = $this->entityManager;
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL p.{id}', 'customer')
->from(Project::class, 'p')
->leftJoin('p.customer', 'customer')
->andWhere($qb->expr()->in('p.id', $ids))
->getQuery()
->execute();
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL p.{id}', 'meta')
->from(Project::class, 'p')
->leftJoin('p.meta', 'meta')
->andWhere($qb->expr()->in('p.id', $ids))
->getQuery()
->execute();
}
}

View File

@@ -0,0 +1,38 @@
<?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\Repository\Loader;
use App\Entity\Project;
use Doctrine\ORM\EntityManagerInterface;
final class ProjectLoader implements LoaderInterface
{
/**
* @var ProjectIdLoader
*/
private $loader;
public function __construct(EntityManagerInterface $entityManager)
{
$this->loader = new ProjectIdLoader($entityManager);
}
/**
* @param Project[] $projects
*/
public function loadResults(array $projects): void
{
$ids = array_map(function (Project $project) {
return $project->getId();
}, $projects);
$this->loader->loadResults($ids);
}
}

View File

@@ -9,12 +9,11 @@
namespace App\Repository\Paginator; namespace App\Repository\Paginator;
use App\Repository\Loader\TimesheetLoader; use App\Repository\Loader\LoaderInterface;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Adapter\AdapterInterface;
final class TimesheetPaginator implements AdapterInterface final class LoaderPaginator implements PaginatorInterface
{ {
/** /**
* @var QueryBuilder * @var QueryBuilder
@@ -25,15 +24,15 @@ final class TimesheetPaginator implements AdapterInterface
*/ */
private $results = 0; private $results = 0;
/** /**
* @var TimesheetLoader * @var LoaderInterface
*/ */
private $loader; private $loader;
public function __construct(QueryBuilder $query, int $results) public function __construct(LoaderInterface $loader, QueryBuilder $query, int $results)
{ {
$this->loader = $loader;
$this->query = $query; $this->query = $query;
$this->results = $results; $this->results = $results;
$this->loader = new TimesheetLoader($query->getEntityManager());
} }
/** /**
@@ -44,15 +43,6 @@ final class TimesheetPaginator implements AdapterInterface
return $this->results; return $this->results;
} }
private function getResults(Query $query)
{
$results = $query->execute();
$this->loader->loadResults($results);
return $results;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -66,7 +56,16 @@ final class TimesheetPaginator implements AdapterInterface
return $this->getResults($query); return $this->getResults($query);
} }
public function getAll() private function getResults(Query $query)
{
$results = $query->execute();
$this->loader->loadResults($results);
return $results;
}
public function getAll(): iterable
{ {
return $this->getResults($this->query->getQuery()); return $this->getResults($this->query->getQuery());
} }

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\Repository\Paginator;
use Pagerfanta\Adapter\AdapterInterface;
interface PaginatorInterface extends AdapterInterface
{
/**
* Returns all available results without pagination.
*
* @return iterable
*/
public function getAll(): iterable;
}

View File

@@ -10,11 +10,15 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Model\ProjectStatistic; use App\Model\ProjectStatistic;
use App\Repository\Loader\ProjectLoader;
use App\Repository\Paginator\LoaderPaginator;
use App\Repository\Paginator\PaginatorInterface;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Repository\Query\ProjectQuery; use App\Repository\Query\ProjectQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@@ -23,7 +27,7 @@ use Pagerfanta\Pagerfanta;
/** /**
* Class ProjectRepository * Class ProjectRepository
*/ */
class ProjectRepository extends AbstractRepository class ProjectRepository extends EntityRepository
{ {
/** /**
* @param Project $project * @param Project $project
@@ -37,15 +41,6 @@ class ProjectRepository extends AbstractRepository
$entityManager->flush(); $entityManager->flush();
} }
/**
* @param int $id
* @return null|Project
*/
public function getById($id)
{
return $this->find($id);
}
/** /**
* @param null|bool $visible * @param null|bool $visible
* @return int * @return int
@@ -99,53 +94,28 @@ class ProjectRepository extends AbstractRepository
/** /**
* Returns a query builder that is used for ProjectType and your own 'query_builder' option. * Returns a query builder that is used for ProjectType and your own 'query_builder' option.
* *
* @param Project|int|null $entity * @param ProjectFormTypeQuery $query
* @param Customer|int|null $customer * @return QueryBuilder
* @return array|QueryBuilder|Pagerfanta
*/ */
public function builderForEntityType($entity = null, $customer = null) public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder
{
$query = new ProjectQuery();
$query->setHiddenEntity($entity);
$query->setCustomer($customer);
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
$query->setOrderBy('name');
return $this->findByQuery($query);
}
/**
* @param ProjectQuery $query
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(ProjectQuery $query)
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
// if we join activities, the max-per-page limit will limit the list $qb
// due to the raised amount of rows by projects * activities ->select('p', 'c')
$qb->select('p', 'c')
->from(Project::class, 'p') ->from(Project::class, 'p')
->join('p.customer', 'c') ->leftJoin('p.customer', 'c')
->orderBy('p.' . $query->getOrderBy(), $query->getOrder()); ->addOrderBy('c.name', 'ASC')
->addOrderBy('p.name', 'ASC')
;
if (ProjectQuery::SHOW_VISIBLE == $query->getVisibility()) { $qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
if (!$query->isExclusiveVisibility()) { $qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
$qb->andWhere($qb->expr()->eq('c.visible', ':visible')); $qb->setParameter('visible', true, \PDO::PARAM_BOOL);
} $qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
$qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
$entity = $query->getHiddenEntity(); if (null !== $query->getProject()) {
if (null !== $entity) { $qb->orWhere('p.id = :project')->setParameter('project', $query->getProject());
$qb->orWhere('p.id = :project')->setParameter('project', $entity);
}
// TODO check for visibility of customer
} elseif (ProjectQuery::SHOW_HIDDEN == $query->getVisibility()) {
$qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
// TODO check for visibility of customer
} }
if (null !== $query->getCustomer()) { if (null !== $query->getCustomer()) {
@@ -153,12 +123,85 @@ class ProjectRepository extends AbstractRepository
->setParameter('customer', $query->getCustomer()); ->setParameter('customer', $query->getCustomer());
} }
if (!empty($query->getIgnoredEntities())) { if (null !== $query->getProjectToIgnore()) {
$qb->andWhere('p.id NOT IN(:ignored)'); $qb->andWhere($qb->expr()->neq('p.id', ':ignored'));
$qb->setParameter('ignored', $query->getIgnoredEntities()); $qb->setParameter('ignored', $query->getProjectToIgnore());
} }
return $this->getBaseQueryResult($qb, $query); return $qb;
}
private function getQueryBuilderForQuery(ProjectQuery $query): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('p')
->from(Project::class, 'p')
;
if (in_array($query->getVisibility(), [ProjectQuery::SHOW_VISIBLE, ProjectQuery::SHOW_HIDDEN])) {
$qb
->leftJoin('p.customer', 'c')
->andWhere($qb->expr()->eq('p.visible', ':visible'))
->andWhere($qb->expr()->eq('c.visible', ':customer_visible'))
;
if (ProjectQuery::SHOW_VISIBLE === $query->getVisibility()) {
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
} elseif (ProjectQuery::SHOW_HIDDEN === $query->getVisibility()) {
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
}
$qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
}
if (null !== $query->getCustomer()) {
$qb->andWhere('p.customer = :customer')
->setParameter('customer', $query->getCustomer());
}
$qb->orderBy('p.' . $query->getOrderBy(), $query->getOrder());
return $qb;
}
public function getPagerfantaForQuery(ProjectQuery $query): Pagerfanta
{
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
$paginator->setMaxPerPage($query->getPageSize());
$paginator->setCurrentPage($query->getPage());
return $paginator;
}
private function getPaginatorForQuery(ProjectQuery $query): PaginatorInterface
{
$qb = $this->getQueryBuilderForQuery($query);
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->countDistinct('p.id'))
;
$counter = (int) $qb->getQuery()->getSingleScalarResult();
$qb = $this->getQueryBuilderForQuery($query);
return new LoaderPaginator(new ProjectLoader($qb->getEntityManager()), $qb, $counter);
}
/**
* @param ProjectQuery $query
* @return Project[]
*/
public function getProjectsForQuery(ProjectQuery $query): iterable
{
$qb = $this->getQueryBuilderForQuery($query);
$results = $qb->getQuery()->execute();
$loader = new ProjectLoader($qb->getEntityManager());
$loader->loadResults($results);
return $results;
} }
/** /**

View File

@@ -0,0 +1,97 @@
<?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\Repository\Query;
use App\Entity\Activity;
use App\Entity\Project;
final class ActivityFormTypeQuery
{
/**
* @var Activity|int|null
*/
private $activity;
/**
* @var Project|int|null
*/
private $project;
/**
* @var Activity|null
*/
private $activityToIgnore;
/**
* @param Activity|int|null $activity
* @param Project|int|null $project
*/
public function __construct($activity = null, $project = null)
{
$this->activity = $activity;
$this->project = $project;
}
/**
* @return Activity|int|null
*/
public function getActivity()
{
return $this->activity;
}
/**
* @param Activity|int|null $activity
* @return ActivityFormTypeQuery
*/
public function setActivity($activity): ActivityFormTypeQuery
{
$this->activity = $activity;
return $this;
}
/**
* @return Project|int|null
*/
public function getProject()
{
return $this->project;
}
/**
* @param Project|int|null $project
* @return ActivityFormTypeQuery
*/
public function setProject($project): ActivityFormTypeQuery
{
$this->project = $project;
return $this;
}
/**
* @return Activity|null
*/
public function getActivityToIgnore(): ?Activity
{
return $this->activityToIgnore;
}
public function setActivityToIgnore(Activity $activityToIgnore): ActivityFormTypeQuery
{
$this->activityToIgnore = $activityToIgnore;
return $this;
}
public function isGlobalsOnly(): bool
{
return null === $this->activity && null === $this->project;
}
}

View File

@@ -19,29 +19,16 @@ class ActivityQuery extends ProjectQuery
/** /**
* @var Project|int|null * @var Project|int|null
*/ */
protected $project; private $project;
/** /**
* @var bool * @var bool
*/ */
protected $orderGlobalsFirst = false; private $globalsOnly = false;
/**
* @var bool
*/
protected $globalsOnly = false;
/** public function __construct()
* @return bool
*/
public function isOrderGlobalsFirst(): bool
{ {
return $this->orderGlobalsFirst; parent::__construct();
} $this->setOrderBy('name');
public function setOrderGlobalsFirst(bool $orderGlobalsFirst): ActivityQuery
{
$this->orderGlobalsFirst = $orderGlobalsFirst;
return $this;
} }
/** /**
@@ -81,4 +68,24 @@ class ActivityQuery extends ProjectQuery
return $this; return $this;
} }
/**
* {@inheritdoc}
*/
public function isDirty(): bool
{
if (parent::isDirty()) {
return true;
}
if ($this->project !== null) {
return true;
}
if ($this->globalsOnly !== false) {
return true;
}
return false;
}
} }

View File

@@ -25,29 +25,25 @@ class BaseQuery
public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder'; public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
/** /**
* @var object|null * @var int
*/ */
protected $hiddenEntity; private $page = self::DEFAULT_PAGE;
/** /**
* @var int * @var int
*/ */
protected $page = self::DEFAULT_PAGE; private $pageSize = self::DEFAULT_PAGESIZE;
/**
* @var int
*/
protected $pageSize = self::DEFAULT_PAGESIZE;
/** /**
* @var string * @var string
*/ */
protected $orderBy = 'id'; private $orderBy = 'id';
/** /**
* @var string * @var string
*/ */
protected $order = self::ORDER_ASC; private $order = self::ORDER_ASC;
/** /**
* @var string * @var string
*/ */
protected $resultType = self::RESULT_TYPE_PAGER; private $resultType = self::RESULT_TYPE_PAGER;
/** /**
* @return int * @return int
@@ -68,10 +64,7 @@ class BaseQuery
return $this; return $this;
} }
/** public function getPageSize(): int
* @return int
*/
public function getPageSize()
{ {
return $this->pageSize; return $this->pageSize;
} }
@@ -82,17 +75,14 @@ class BaseQuery
*/ */
public function setPageSize($pageSize) public function setPageSize($pageSize)
{ {
if (!empty($pageSize) && (int) $pageSize > 0) { if ($pageSize !== null && (int) $pageSize > 0) {
$this->pageSize = (int) $pageSize; $this->pageSize = (int) $pageSize;
} }
return $this; return $this;
} }
/** public function getOrderBy(): string
* @return string
*/
public function getOrderBy()
{ {
return $this->orderBy; return $this->orderBy;
} }
@@ -110,10 +100,7 @@ class BaseQuery
return $this; return $this;
} }
/** public function getOrder(): string
* @return string
*/
public function getOrder()
{ {
return $this->order; return $this->order;
} }
@@ -132,6 +119,7 @@ class BaseQuery
} }
/** /**
* @deprecated since 1.0
* @return string * @return string
*/ */
public function getResultType() public function getResultType()
@@ -140,6 +128,7 @@ class BaseQuery
} }
/** /**
* @deprecated since 1.0
* @param string $resultType * @param string $resultType
* @return $this * @return $this
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
@@ -158,21 +147,20 @@ class BaseQuery
} }
/** /**
* @return object|null * Returns whether the query has changed fields, compared to the original state.
*
* @return bool
*/ */
public function getHiddenEntity() public function isDirty(): bool
{ {
return $this->hiddenEntity; if ($this->page !== self::DEFAULT_PAGE) {
} return true;
}
/** if ($this->pageSize !== self::DEFAULT_PAGESIZE) {
* @param object|string|null $hiddenEntity return true;
* @return BaseQuery }
*/
public function setHiddenEntity($hiddenEntity)
{
$this->hiddenEntity = $hiddenEntity;
return $this; return false;
} }
} }

View File

@@ -0,0 +1,69 @@
<?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\Repository\Query;
use App\Entity\Customer;
/**
* Can be used for advanced queries with the: CustomerRepository
*/
final class CustomerFormTypeQuery
{
/**
* @var Customer|int|null
*/
private $customer;
/**
* @var Customer|null
*/
private $customerToIgnore;
/**
* @param Customer|int|null $customer
*/
public function __construct($customer = null)
{
$this->customer = $customer;
}
/**
* @return Customer|int|null
*/
public function getCustomer()
{
return $this->customer;
}
/**
* @param Customer|int|null $customer
* @return $this
*/
public function setCustomer($customer): CustomerFormTypeQuery
{
$this->customer = $customer;
return $this;
}
/**
* @return Customer|null
*/
public function getCustomerToIgnore(): ?Customer
{
return $this->customerToIgnore;
}
public function setCustomerToIgnore(Customer $customerToIgnore): CustomerFormTypeQuery
{
$this->customerToIgnore = $customerToIgnore;
return $this;
}
}

View File

@@ -9,34 +9,13 @@
namespace App\Repository\Query; namespace App\Repository\Query;
use App\Entity\Customer;
/** /**
* Can be used for advanced queries with the: CustomerRepository * Can be used for advanced queries with the: CustomerRepository
*/ */
class CustomerQuery extends VisibilityQuery class CustomerQuery extends VisibilityQuery
{ {
/** public function __construct()
* @var array
*/
protected $ignored = [];
/**
* @param Customer|int $customer
* @return $this
*/
public function addIgnoredEntity($customer)
{ {
$this->ignored[] = $customer; $this->setOrderBy('name');
return $this;
}
/**
* @return array
*/
public function getIgnoredEntities()
{
return $this->ignored;
} }
} }

View File

@@ -14,7 +14,7 @@ class ExportQuery extends TimesheetQuery
/** /**
* @var string * @var string
*/ */
protected $type; private $type;
/** /**
* @return string * @return string

View File

@@ -19,14 +19,7 @@ class InvoiceQuery extends TimesheetQuery
/** /**
* @var InvoiceTemplate * @var InvoiceTemplate
*/ */
protected $template; private $template;
/**
* TODO can this be removed ???
*
* @var InvoiceTemplate[]
*/
protected $templates = [];
/** /**
* @return InvoiceTemplate * @return InvoiceTemplate

View File

@@ -0,0 +1,92 @@
<?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\Repository\Query;
use App\Entity\Customer;
use App\Entity\Project;
final class ProjectFormTypeQuery
{
/**
* @var Customer|int|null
*/
private $customer;
/**
* @var Project|int|null
*/
private $project;
/**
* @var Project|null
*/
private $projectToIgnore;
/**
* @param Project|int|null $project
* @param Customer|int|null $customer
*/
public function __construct($project = null, $customer = null)
{
$this->project = $project;
$this->customer = $customer;
}
/**
* @return Customer|int|null
*/
public function getCustomer()
{
return $this->customer;
}
/**
* @param Customer|int|null $customer
* @return $this
*/
public function setCustomer($customer): ProjectFormTypeQuery
{
$this->customer = $customer;
return $this;
}
/**
* @return Project|int|null
*/
public function getProject()
{
return $this->project;
}
/**
* @param Project|int|null $project
* @return ProjectFormTypeQuery
*/
public function setProject($project): ProjectFormTypeQuery
{
$this->project = $project;
return $this;
}
/**
* @return Project|null
*/
public function getProjectToIgnore(): ?Project
{
return $this->projectToIgnore;
}
public function setProjectToIgnore(Project $projectToIgnore): ProjectFormTypeQuery
{
$this->projectToIgnore = $projectToIgnore;
return $this;
}
}

View File

@@ -14,35 +14,17 @@ use App\Entity\Customer;
/** /**
* Can be used for advanced queries with the: ProjectRepository * Can be used for advanced queries with the: ProjectRepository
*/ */
class ProjectQuery extends VisibilityQuery class ProjectQuery extends CustomerQuery
{ {
/** /**
* @var Customer|int|null * @var Customer|int|null
*/ */
protected $customer; private $customer;
/** public function __construct()
* @var array
*/
protected $ignored = [];
/**
* @param mixed $entity
* @return $this
*/
public function addIgnoredEntity($entity)
{ {
$this->ignored[] = $entity; parent::__construct();
$this->setOrderBy('name');
return $this;
}
/**
* @return array
*/
public function getIgnoredEntities()
{
return $this->ignored;
} }
/** /**
@@ -63,4 +45,20 @@ class ProjectQuery extends VisibilityQuery
return $this; return $this;
} }
/**
* {@inheritdoc}
*/
public function isDirty(): bool
{
if (parent::isDirty()) {
return true;
}
if ($this->customer !== null) {
return true;
}
return false;
}
} }

View File

@@ -25,16 +25,6 @@ class TimesheetQuery extends ActivityQuery
public const STATE_EXPORTED = 4; public const STATE_EXPORTED = 4;
public const STATE_NOT_EXPORTED = 5; public const STATE_NOT_EXPORTED = 5;
/**
* Overwritten for different default order
* @var string
*/
protected $order = self::ORDER_DESC;
/**
* Overwritten for different default order
* @var string
*/
protected $orderBy = 'begin';
/** /**
* @var User|null * @var User|null
*/ */
@@ -62,6 +52,9 @@ class TimesheetQuery extends ActivityQuery
public function __construct() public function __construct()
{ {
parent::__construct();
$this->setOrder(self::ORDER_DESC);
$this->setOrderBy('begin');
$this->dateRange = new DateRange(); $this->dateRange = new DateRange();
} }
@@ -245,4 +238,40 @@ class TimesheetQuery extends ActivityQuery
return $this; return $this;
} }
/**
* {@inheritdoc}
*/
public function isDirty(): bool
{
if (parent::isDirty()) {
return true;
}
if ($this->activity !== null) {
return true;
}
if (!empty($this->tags)) {
return true;
}
if ($this->user !== null) {
return true;
}
if ($this->state !== self::STATE_ALL) {
return true;
}
if ($this->exported !== self::STATE_ALL) {
return true;
}
if ($this->dateRange->getBegin() !== null || $this->dateRange->getEnd() !== null) {
return true;
}
return false;
}
} }

View File

@@ -18,14 +18,16 @@ class VisibilityQuery extends BaseQuery
public const SHOW_HIDDEN = 2; public const SHOW_HIDDEN = 2;
public const SHOW_BOTH = 3; public const SHOW_BOTH = 3;
public const ALLOWED_VISIBILITY_STATES = [
self::SHOW_BOTH,
self::SHOW_VISIBLE,
self::SHOW_HIDDEN,
];
/** /**
* @var int * @var int
*/ */
protected $visibility = self::SHOW_VISIBLE; private $visibility = self::SHOW_VISIBLE;
/**
* @var bool
*/
protected $exclusiveVisibility = false;
/** /**
* @return int * @return int
@@ -46,7 +48,7 @@ class VisibilityQuery extends BaseQuery
} }
$visibility = (int) $visibility; $visibility = (int) $visibility;
if (in_array($visibility, [self::SHOW_BOTH, self::SHOW_VISIBLE, self::SHOW_HIDDEN], true)) { if (in_array($visibility, self::ALLOWED_VISIBILITY_STATES, true)) {
$this->visibility = $visibility; $this->visibility = $visibility;
} }
@@ -54,23 +56,18 @@ class VisibilityQuery extends BaseQuery
} }
/** /**
* @return bool * {@inheritdoc}
*/ */
public function isExclusiveVisibility() public function isDirty(): bool
{ {
return $this->exclusiveVisibility; if (parent::isDirty()) {
} return true;
}
/** if ($this->visibility !== self::SHOW_VISIBLE) {
* If set to true, this will ONLY filter the visibility on the main queried object. return true;
* }
* @param bool $exclusiveVisibility
* @return $this
*/
public function setExclusiveVisibility($exclusiveVisibility)
{
$this->exclusiveVisibility = (bool) $exclusiveVisibility;
return $this; return false;
} }
} }

View File

@@ -16,11 +16,12 @@ use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
/** /**
* Trait RepositoryTrait * @deprecated since 1.0
*/ */
trait RepositoryTrait trait RepositoryTrait
{ {
/** /**
* @deprecated since 1.0
* @param QueryBuilder $qb * @param QueryBuilder $qb
* @param BaseQuery $query * @param BaseQuery $query
* @return QueryBuilder|Pagerfanta|array * @return QueryBuilder|Pagerfanta|array
@@ -42,7 +43,7 @@ trait RepositoryTrait
* @param int $maxPerPage * @param int $maxPerPage
* @return Pagerfanta * @return Pagerfanta
*/ */
protected function getPager(Query $query, $page = 1, $maxPerPage = 25) private function getPager(Query $query, $page = 1, $maxPerPage = 25)
{ {
$paginator = new Pagerfanta(new DoctrineORMAdapter($query, false)); $paginator = new Pagerfanta(new DoctrineORMAdapter($query, false));
$paginator->setMaxPerPage($maxPerPage); $paginator->setMaxPerPage($maxPerPage);

View File

@@ -10,9 +10,12 @@
namespace App\Repository; namespace App\Repository;
use App\Repository\Query\TagQuery; use App\Repository\Query\TagQuery;
use Doctrine\ORM\EntityRepository;
class TagRepository extends AbstractRepository class TagRepository extends EntityRepository
{ {
use RepositoryTrait;
/** /**
* Find ids of the given tagNames separated by comma * Find ids of the given tagNames separated by comma
* @param string $tagNames * @param string $tagNames

View File

@@ -17,7 +17,8 @@ use App\Model\Statistic\Month;
use App\Model\Statistic\Year; use App\Model\Statistic\Year;
use App\Model\TimesheetStatistic; use App\Model\TimesheetStatistic;
use App\Repository\Loader\TimesheetLoader; use App\Repository\Loader\TimesheetLoader;
use App\Repository\Paginator\TimesheetPaginator; use App\Repository\Paginator\LoaderPaginator;
use App\Repository\Paginator\PaginatorInterface;
use App\Repository\Query\TimesheetQuery; use App\Repository\Query\TimesheetQuery;
use DateTime; use DateTime;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
@@ -349,24 +350,23 @@ class TimesheetRepository extends EntityRepository
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('t', 'a', 'p', 'c') $qb->select('t')
->from(Timesheet::class, 't') ->from(Timesheet::class, 't')
->join('t.activity', 'a')
->join('t.project', 'p')
->join('p.customer', 'c')
->leftJoin('t.tags', 'tags')
->andWhere($qb->expr()->isNotNull('t.begin')) ->andWhere($qb->expr()->isNotNull('t.begin'))
->andWhere($qb->expr()->isNull('t.end')) ->andWhere($qb->expr()->isNull('t.end'))
->orderBy('t.begin', 'DESC'); ->orderBy('t.begin', 'DESC');
$params = [];
if (null !== $user) { if (null !== $user) {
$qb->andWhere('t.user = :user'); $qb->andWhere('t.user = :user');
$params['user'] = $user; $qb->setParameter('user', $user);
} }
return $qb->getQuery()->execute($params); $results = $qb->getQuery()->getResult();
$loader = new TimesheetLoader($qb->getEntityManager());
$loader->loadResults($results);
return $results;
} }
/** /**
@@ -414,36 +414,42 @@ class TimesheetRepository extends EntityRepository
return $paginator; return $paginator;
} }
protected function getPaginatorForQuery(TimesheetQuery $query): TimesheetPaginator protected function getPaginatorForQuery(TimesheetQuery $query): PaginatorInterface
{ {
$qb = $this->getQueryBuilderForQuery($query); $qb = $this->getQueryBuilderForQuery($query);
$qb->select($qb->expr()->countDistinct('t.id'))->resetDQLPart('orderBy'); $qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->countDistinct('t.id'))
;
$counter = (int) $qb->getQuery()->getSingleScalarResult(); $counter = (int) $qb->getQuery()->getSingleScalarResult();
$qb = $this->getQueryBuilderForQuery($query); $qb = $this->getQueryBuilderForQuery($query);
$qb->select('t');
$paginator = new TimesheetPaginator($qb, $counter); return new LoaderPaginator(new TimesheetLoader($qb->getEntityManager()), $qb, $counter);
return $paginator;
} }
/** /**
* @param TimesheetQuery $query * @param TimesheetQuery $query
* @return Timesheet[] * @return Timesheet[]
*/ */
public function getTimesheetsForQuery(TimesheetQuery $query): array public function getTimesheetsForQuery(TimesheetQuery $query): iterable
{ {
// this is using the paginator internally, as it will load all joined entities into the working unit
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
$paginator = $this->getPaginatorForQuery($query); $paginator = $this->getPaginatorForQuery($query);
return $paginator->getAll(); return $paginator->getAll();
} }
protected function getQueryBuilderForQuery(TimesheetQuery $query): QueryBuilder private function getQueryBuilderForQuery(TimesheetQuery $query): QueryBuilder
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Timesheet::class, 't'); $qb
->select('t')
->from(Timesheet::class, 't')
;
if (null !== $query->getUser()) { if (null !== $query->getUser()) {
$qb->andWhere('t.user = :user') $qb->andWhere('t.user = :user')

View File

@@ -11,15 +11,25 @@ namespace App\Repository;
use App\Entity\User; use App\Entity\User;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
class UserRepository extends AbstractRepository implements UserLoaderInterface class UserRepository extends EntityRepository implements UserLoaderInterface
{ {
use RepositoryTrait;
public function getById($id): ?User
{
@trigger_error('UserRepository::getById is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
return $this->getUserById($id);
}
/** /**
* @param int $id * @param int $id
* @return null|User * @return null|User
*/ */
public function getById($id) public function getUserById($id): ?User
{ {
return $this->find($id); return $this->find($id);
} }

View File

@@ -50,6 +50,6 @@ class CurrentUser
return null; return null;
} }
return $this->repository->getById($user->getId()); return $this->repository->getUserById($user->getId());
} }
} }

View File

@@ -224,10 +224,11 @@
$('#type').val($(this).attr('data-type')); $('#type').val($(this).attr('data-type'));
var $form = $("#export-form"); var $form = $("#export-form");
var prevAction = $form.attr('action'); var prevAction = $form.attr('action');
$form.attr('target', '_blank').attr('action', '{{ path('export_data') }}'); var prevMethod = $form.attr('method');
$form.attr('target', '_blank').attr('method', 'POST').attr('action', '{{ path('export_data') }}');
$form.submit(); $form.submit();
$('#type').val(''); $('#type').val('');
$form.removeAttr('target').attr('action', prevAction); $form.removeAttr('target').attr('action', prevAction).attr('method', prevMethod);
}); });
}); });
</script> </script>

View File

@@ -71,7 +71,7 @@
{% if is_granted('create_invoice') %} {% if is_granted('create_invoice') %}
<div class="row no-print"> <div class="row no-print">
<div class="col-xs-12"> <div class="col-xs-12">
<a href="{{ path('invoice_print') }}" class="btn btn-success pull-right toolbar-action"> <a href="{{ path('invoice_print') }}" class="btn btn-success pull-right toolbar-action" data-method="POST">
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }} <i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
</a> </a>
</div> </div>

View File

@@ -91,7 +91,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
); );
$this->assertEquals( $this->assertEquals(
Response::HTTP_FORBIDDEN, // TODO that should actually be Response::HTTP_UNAUTHORIZED Response::HTTP_FORBIDDEN,
$response->getStatusCode(), $response->getStatusCode(),
sprintf('The secure URL %s has the wrong status code %s.', $url, $response->getStatusCode()) sprintf('The secure URL %s has the wrong status code %s.', $url, $response->getStatusCode())
); );

View File

@@ -77,22 +77,23 @@ class ActivityControllerTest extends APIControllerBaseTest
$hasProject = $expected[$i][0]; $hasProject = $expected[$i][0];
$this->assertStructure($activity, false); $this->assertStructure($activity, false);
if ($hasProject) { if ($hasProject) {
$this->assertEquals($expected[$i][0], $activity['project']); $this->assertEquals($expected[$i][1], $activity['project']);
} }
} }
} }
public function getCollectionTestData() public function getCollectionTestData()
{ {
yield ['/api/activities', [], [[false], [false], [true, 2], [true, 1], [true, 2]]]; yield ['/api/activities', [], [[false], [true, 2], [true, 2], [null], [true, 1]]];
//yield ['/api/activities', [], [[false], [false], [true, 2], [true, 1], [true, 2]]];
yield ['/api/activities', ['globals' => 'true'], [[false], [false]]]; yield ['/api/activities', ['globals' => 'true'], [[false], [false]]];
yield ['/api/activities', ['globals' => 'true', 'visible' => 3], [[false], [false], [false]]]; yield ['/api/activities', ['globals' => 'true', 'visible' => 3], [[false], [false], [false]]];
yield ['/api/activities', ['globals' => 'true', 'visible' => '2'], [[false]]]; yield ['/api/activities', ['globals' => 'true', 'visible' => '2'], [[false]]];
yield ['/api/activities', ['globals' => 'true', 'visible' => 1], [[false], [false]]]; yield ['/api/activities', ['globals' => 'true', 'visible' => 1], [[false], [false]]];
yield ['/api/activities', ['project' => '1'], [[false], [false], [true, 1]]]; yield ['/api/activities', ['project' => '1'], [[false], [false], [true, 1]]];
yield ['/api/activities', ['project' => '2', 'visible' => 1], [[false], [false], [true, 2], [true, 2]]]; yield ['/api/activities', ['project' => '2', 'visible' => 1], [[false], [true, 2], [true, 2], [false]]];
yield ['/api/activities', ['project' => '2', 'visible' => '3'], [[false], [false], [false], [true, 2], [true, 2], [true, 2]]]; yield ['/api/activities', ['project' => '2', 'visible' => '3'], [[false], [true, 2], [true, 2], [true, 2], [false], [false]]];
yield ['/api/activities', ['project' => '2', 'visible' => 2], [[false], [true, 2]]]; yield ['/api/activities', ['project' => '2', 'visible' => 2], [[true, 2], [false]]];
} }
public function testGetCollectionWithQuery() public function testGetCollectionWithQuery()
@@ -100,7 +101,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->loadActivityTestData($client); $this->loadActivityTestData($client);
$query = ['order' => 'ASC', 'orderBy' => 'project', 'globalsFirst' => 'false']; $query = ['order' => 'ASC', 'orderBy' => 'project'];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/activities', 'GET', $query); $this->assertAccessIsGranted($client, '/api/activities', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
@@ -220,7 +221,9 @@ class ActivityControllerTest extends APIControllerBaseTest
protected function assertStructure(array $result, $full = true) protected function assertStructure(array $result, $full = true)
{ {
$expectedKeys = ['id', 'name', 'visible', 'project', 'hourlyRate', 'fixedRate', 'color', 'metaFields']; $expectedKeys = [
'id', 'name', 'visible', 'project', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle'
];
if ($full) { if ($full) {
$expectedKeys = array_merge($expectedKeys, [ $expectedKeys = array_merge($expectedKeys, [

View File

@@ -163,7 +163,9 @@ class CustomerControllerTest extends APIControllerBaseTest
protected function assertStructure(array $result, $full = true) protected function assertStructure(array $result, $full = true)
{ {
$expectedKeys = ['id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields']; $expectedKeys = [
'id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields'
];
if ($full) { if ($full) {
$expectedKeys = array_merge($expectedKeys, [ $expectedKeys = array_merge($expectedKeys, [

View File

@@ -100,9 +100,11 @@ class ProjectControllerTest extends APIControllerBaseTest
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_VISIBLE], [[true, 1], [false, 1]]]; yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_VISIBLE], [[true, 1], [false, 1]]];
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]]; yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]];
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false, 1]]]; yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false, 1]]];
// customer is invisible, so nothing should be returned
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_VISIBLE], []]; yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_VISIBLE], []];
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_BOTH], [[false, 2], [false, 2]]]; yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_BOTH], [[false, 2], [false, 2]]];
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false, 2]]]; // customer is invisible, so nothing should be returned
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_HIDDEN], []];
} }
public function testGetEntity() public function testGetEntity()
@@ -212,7 +214,7 @@ class ProjectControllerTest extends APIControllerBaseTest
protected function assertStructure(array $result, $full = true) protected function assertStructure(array $result, $full = true)
{ {
$expectedKeys = [ $expectedKeys = [
'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields' 'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle'
]; ];
if ($full) { if ($full) {

View File

@@ -67,7 +67,7 @@ class ExportControllerTest extends ControllerBaseTest
public function testExportActionWithMissingRenderer() public function testExportActionWithMissingRenderer()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->request($client, '/export/data'); $this->request($client, '/export/data', 'POST');
$response = $client->getResponse(); $response = $client->getResponse();
$this->assertFalse($response->isSuccessful()); $this->assertFalse($response->isSuccessful());
@@ -79,11 +79,14 @@ class ExportControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->request($client, '/export/'); $this->request($client, '/export/', 'GET');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('#export-form')->form(); $form = $client->getCrawler()->filter('#export-form')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/export/data')); $node = $form->getFormNode();
$node->setAttribute('action', $this->createUrl('/export/data'));
$node->setAttribute('method', 'POST');
$client->submit($form, [ $client->submit($form, [
'type' => 'default' 'type' => 'default'
]); ]);
@@ -112,7 +115,10 @@ class ExportControllerTest extends ControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('#export-form')->form(); $form = $client->getCrawler()->filter('#export-form')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/export/data')); $node = $form->getFormNode();
$node->setAttribute('action', $this->createUrl('/export/data'));
$node->setAttribute('method', 'POST');
// don't add daterange to make sure the current month is the default range // don't add daterange to make sure the current month is the default range
$client->submit($form, [ $client->submit($form, [
'type' => 'html' 'type' => 'html'

View File

@@ -158,7 +158,9 @@ class InvoiceControllerTest extends ControllerBaseTest
$this->assertDataTableRowCount($client, 'datatable_invoice', 20); $this->assertDataTableRowCount($client, 'datatable_invoice', 20);
$form = $client->getCrawler()->filter('#invoice-print-form')->form(); $form = $client->getCrawler()->filter('#invoice-print-form')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/invoice/print')); $node = $form->getFormNode();
$node->setAttribute('action', $this->createUrl('/invoice/print'));
$node->setAttribute('method', 'POST');
$client->submit($form, [ $client->submit($form, [
'template' => 1, 'template' => 1,
'user' => '', 'user' => '',
@@ -177,7 +179,7 @@ class InvoiceControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->request($client, '/invoice/print'); $this->request($client, '/invoice/print', 'POST');
$this->assertIsRedirect($client, '/invoice/template/create'); $this->assertIsRedirect($client, '/invoice/template/create');
} }
@@ -189,7 +191,7 @@ class InvoiceControllerTest extends ControllerBaseTest
$fixture = new InvoiceFixtures(); $fixture = new InvoiceFixtures();
$this->importFixture($em, $fixture); $this->importFixture($em, $fixture);
$this->request($client, '/invoice/print'); $this->request($client, '/invoice/print', 'POST');
$this->assertIsRedirect($client, '/invoice/'); $this->assertIsRedirect($client, '/invoice/');
} }

View File

@@ -35,8 +35,8 @@ class UserDateTimeFactoryFactory extends AbstractMockFactory
$pref->setValue($timezone); $pref->setValue($timezone);
$user->addPreference($pref); $user->addPreference($pref);
} }
$repository = $this->getMockBuilder(UserRepository::class)->setMethods(['getById'])->disableOriginalConstructor()->getMock(); $repository = $this->getMockBuilder(UserRepository::class)->setMethods(['getUserById'])->disableOriginalConstructor()->getMock();
$repository->expects(TestCase::exactly(1))->method('getById')->willReturn($user); $repository->expects(TestCase::exactly(1))->method('getUserById')->willReturn($user);
$token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock(); $token = $this->getMockBuilder(UsernamePasswordToken::class)->setMethods(['getUser'])->disableOriginalConstructor()->getMock();
$token->expects(TestCase::exactly(1))->method('getUser')->willReturn($user); $token->expects(TestCase::exactly(1))->method('getUser')->willReturn($user);
$tokenStorage = new TokenStorage(); $tokenStorage = new TokenStorage();

View File

@@ -23,7 +23,7 @@ class ActivityQueryTest extends BaseQueryTest
{ {
$sut = new ActivityQuery(); $sut = new ActivityQuery();
$this->assertBaseQuery($sut); $this->assertBaseQuery($sut, 'name');
$this->assertInstanceOf(VisibilityQuery::class, $sut); $this->assertInstanceOf(VisibilityQuery::class, $sut);
$this->assertNull($sut->getCustomer()); $this->assertNull($sut->getCustomer());

View File

@@ -9,7 +9,6 @@
namespace App\Tests\Repository\Query; namespace App\Tests\Repository\Query;
use App\Entity\User;
use App\Repository\Query\BaseQuery; use App\Repository\Query\BaseQuery;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -23,13 +22,12 @@ class BaseQueryTest extends TestCase
$this->assertBaseQuery(new BaseQuery()); $this->assertBaseQuery(new BaseQuery());
} }
protected function assertBaseQuery(BaseQuery $sut) protected function assertBaseQuery(BaseQuery $sut, $orderBy = 'id')
{ {
$this->assertResultType($sut); $this->assertResultType($sut);
$this->assertHiddenEntity($sut);
$this->assertPage($sut); $this->assertPage($sut);
$this->assertPageSize($sut); $this->assertPageSize($sut);
$this->assertOrderBy($sut); $this->assertOrderBy($sut, $orderBy);
$this->assertOrder($sut); $this->assertOrder($sut);
} }
@@ -51,17 +49,6 @@ class BaseQueryTest extends TestCase
} }
} }
protected function assertHiddenEntity(BaseQuery $sut)
{
$this->assertNull($sut->getHiddenEntity());
$actual = new User();
$actual->setUsername('foo-bar');
$sut->setHiddenEntity($actual);
$this->assertEquals($actual, $sut->getHiddenEntity());
}
protected function assertPage(BaseQuery $sut) protected function assertPage(BaseQuery $sut)
{ {
$this->assertEquals(BaseQuery::DEFAULT_PAGE, $sut->getPage()); $this->assertEquals(BaseQuery::DEFAULT_PAGE, $sut->getPage());

View File

@@ -21,7 +21,7 @@ class CustomerQueryTest extends BaseQueryTest
{ {
$sut = new CustomerQuery(); $sut = new CustomerQuery();
$this->assertBaseQuery($sut); $this->assertBaseQuery($sut, 'name');
$this->assertInstanceOf(VisibilityQuery::class, $sut); $this->assertInstanceOf(VisibilityQuery::class, $sut);
} }
} }

View File

@@ -25,7 +25,6 @@ class ExportQueryTest extends BaseQueryTest
$sut = new ExportQuery(); $sut = new ExportQuery();
$this->assertResultType($sut); $this->assertResultType($sut);
$this->assertHiddenEntity($sut);
$this->assertPage($sut); $this->assertPage($sut);
$this->assertPageSize($sut); $this->assertPageSize($sut);
$this->assertOrderBy($sut, 'begin'); $this->assertOrderBy($sut, 'begin');

View File

@@ -22,7 +22,7 @@ class ProjectQueryTest extends BaseQueryTest
{ {
$sut = new ProjectQuery(); $sut = new ProjectQuery();
$this->assertBaseQuery($sut); $this->assertBaseQuery($sut, 'name');
$this->assertInstanceOf(VisibilityQuery::class, $sut); $this->assertInstanceOf(VisibilityQuery::class, $sut);
$this->assertNull($sut->getCustomer()); $this->assertNull($sut->getCustomer());

View File

@@ -25,7 +25,6 @@ class TimesheetQueryTest extends BaseQueryTest
$sut = new TimesheetQuery(); $sut = new TimesheetQuery();
$this->assertResultType($sut); $this->assertResultType($sut);
$this->assertHiddenEntity($sut);
$this->assertPage($sut); $this->assertPage($sut);
$this->assertPageSize($sut); $this->assertPageSize($sut);
$this->assertOrderBy($sut, 'begin'); $this->assertOrderBy($sut, 'begin');

View File

@@ -21,12 +21,8 @@ class VisibilityQueryTest extends TestCase
{ {
$sut = new VisibilityQuery(); $sut = new VisibilityQuery();
$this->assertFalse($sut->isExclusiveVisibility());
$this->assertEquals(VisibilityQuery::SHOW_VISIBLE, $sut->getVisibility()); $this->assertEquals(VisibilityQuery::SHOW_VISIBLE, $sut->getVisibility());
$sut->setExclusiveVisibility(true);
$this->assertTrue($sut->isExclusiveVisibility());
$sut->setVisibility('foo-bar'); $sut->setVisibility('foo-bar');
$this->assertEquals(VisibilityQuery::SHOW_VISIBLE, $sut->getVisibility()); $this->assertEquals(VisibilityQuery::SHOW_VISIBLE, $sut->getVisibility());