support custom fields for timesheets, customers, projects and activities (#871)
This commit is contained in:
@@ -1,15 +1,16 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Kimai is an open source project, contributions made by the community are welcome.
|
Kimai is an open source project, contributions made by the community are welcome.
|
||||||
Send us your ideas, code reviews, pull requests and feature requests to help us improve this project.
|
Send your ideas, code reviews, pull requests and feature requests to help improving this project.
|
||||||
|
|
||||||
## Pull request rules
|
## Pull request rules
|
||||||
|
|
||||||
- We use PSR-2 with some additional code-style checks (see our [php-cs-fixer config](.php_cs.dist)). You can run `bin/console kimai:codestyle` to check and `bin/console kimai:codestyle --fix` to fix violations.
|
- Use the [pre-configured codesniffer](.php_cs.dist)) to check for `composer kimai:codestyle` and fix `composer kimai:codestyle-fix` violations
|
||||||
- Add PHPUnit tests for your changes, verify everything still works and execute our test-suites `bin/console kimai:test-unit` and `bin/console kimai:test-integration`.
|
- Add PHPUnit tests for your changes!
|
||||||
- If you contribute new files, please add them with the file-header template from below (our chode-style fixer can do that for you).
|
- Verify everything still works with `composer kimai:tests-unit` and `composer kimai:tests-integration`
|
||||||
- With sending in a PR, you accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well).
|
- If you contribute new files, add them with the file-header template from below (the code-style fixer can do that for you)
|
||||||
- If one of the PR checks fails, please fix them before asking us for a review.
|
- When sending in a PR, you must accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well), otherwise your PR will be closed
|
||||||
|
- If one of the PR checks/builds fails, fix it before asking for a review
|
||||||
|
|
||||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
[](https://gitter.im/kimai2/support)
|
[](https://gitter.im/kimai2/support)
|
||||||
|
|
||||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||||
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, responsive and mobile-ready etc.
|
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ kimai:
|
|||||||
|
|
||||||
# The time-tracking mode that should be used.
|
# The time-tracking mode that should be used.
|
||||||
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
|
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
|
||||||
mode: default
|
# mode: default
|
||||||
|
|
||||||
# The default time to pre-fill the "create timesheet" form (in some cases).
|
# The default time to pre-fill the "create timesheet" form (in some cases).
|
||||||
# This setting is only respected by some timetracking modes and not in all situations.
|
# This setting is only respected by some timetracking modes and not in all situations.
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ nelmio_api_doc:
|
|||||||
models:
|
models:
|
||||||
use_jms: true
|
use_jms: true
|
||||||
names:
|
names:
|
||||||
- { alias: CustomerEditForm, type: App\Form\CustomerEditForm, groups: [Default, Entity, Customer] }
|
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer] }
|
||||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
||||||
|
- { alias: CustomerMetaField, type: App\Entity\CustomerMeta, groups: [Default, Customer] }
|
||||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||||
- { alias: ProjectEditForm, type: App\Form\ProjectEditForm, groups: [Default, Entity, Project] }
|
- { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project] }
|
||||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
||||||
|
- { alias: ProjectMetaField, type: App\Entity\ProjectMeta, groups: [Default, Project] }
|
||||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||||
- { alias: ActivityEditForm, type: App\Form\ActivityEditForm, groups: [Default, Entity, Activity] }
|
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] }
|
||||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||||
|
- { alias: ActivityMetaField, type: App\Entity\ActivityMeta, groups: [Default, Activity] }
|
||||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||||
- { alias: TimesheetEditForm, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] }
|
- { alias: TimesheetEditForm, type: App\Form\API\TimesheetApiEditForm, groups: [Default, Entity, Timesheet] }
|
||||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
||||||
|
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
|
||||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
||||||
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
||||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\Activity:
|
App\Entity\Activity:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate, color, budget, timeBudget]
|
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -32,9 +32,16 @@ App\Entity\Activity:
|
|||||||
groups: [Default]
|
groups: [Default]
|
||||||
color:
|
color:
|
||||||
include: true
|
include: true
|
||||||
|
metaFields:
|
||||||
|
exclude: true
|
||||||
virtual_properties:
|
virtual_properties:
|
||||||
getProject:
|
getProject:
|
||||||
serialized_name: project
|
serialized_name: project
|
||||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||||
type: integer
|
type: integer
|
||||||
groups: [Default]
|
groups: [Default]
|
||||||
|
getMetaFields:
|
||||||
|
serialized_name: metaFields
|
||||||
|
exp: "object.getVisibleMetaFields()"
|
||||||
|
type: array<App\Entity\ActivityMeta>
|
||||||
|
groups: [Default]
|
||||||
|
|||||||
15
config/serializer/App/Entity.ActivityMeta.yml
Normal file
15
config/serializer/App/Entity.ActivityMeta.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
App\Entity\ActivityMeta:
|
||||||
|
exclusion_policy: All
|
||||||
|
custom_accessor_order: [name, value]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
include: false
|
||||||
|
value:
|
||||||
|
include: false
|
||||||
|
virtual_properties:
|
||||||
|
getName:
|
||||||
|
serialized_name: name
|
||||||
|
exp: "object.isVisible() ? object.getName() : null"
|
||||||
|
getValue:
|
||||||
|
serialized_name: value
|
||||||
|
exp: "object.isVisible() ? object.getValue() : null"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\Customer:
|
App\Entity\Customer:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate, color, budget, timeBudget]
|
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -61,3 +61,11 @@ App\Entity\Customer:
|
|||||||
groups: [Customer]
|
groups: [Customer]
|
||||||
color:
|
color:
|
||||||
include: true
|
include: true
|
||||||
|
metaFields:
|
||||||
|
exclude: true
|
||||||
|
virtual_properties:
|
||||||
|
getMetaFields:
|
||||||
|
serialized_name: metaFields
|
||||||
|
exp: "object.getVisibleMetaFields()"
|
||||||
|
type: array<App\Entity\CustomerMeta>
|
||||||
|
groups: [Default]
|
||||||
|
|||||||
15
config/serializer/App/Entity.CustomerMeta.yml
Normal file
15
config/serializer/App/Entity.CustomerMeta.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
App\Entity\CustomerMeta:
|
||||||
|
exclusion_policy: All
|
||||||
|
custom_accessor_order: [name, value]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
include: false
|
||||||
|
value:
|
||||||
|
include: false
|
||||||
|
virtual_properties:
|
||||||
|
getName:
|
||||||
|
serialized_name: name
|
||||||
|
exp: "object.isVisible() ? object.getName() : null"
|
||||||
|
getValue:
|
||||||
|
serialized_name: value
|
||||||
|
exp: "object.isVisible() ? object.getValue() : null"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\Project:
|
App\Entity\Project:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, name, comment, visible, orderNumber, customer, fixedRate, hourlyRate, color, budget, timeBudget]
|
custom_accessor_order: [id, name, comment, visible, orderNumber, customer, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -30,9 +30,16 @@ App\Entity\Project:
|
|||||||
groups: [Subresource]
|
groups: [Subresource]
|
||||||
color:
|
color:
|
||||||
include: true
|
include: true
|
||||||
|
metaFields:
|
||||||
|
exclude: true
|
||||||
virtual_properties:
|
virtual_properties:
|
||||||
getCustomer:
|
getCustomer:
|
||||||
serialized_name: customer
|
serialized_name: customer
|
||||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
||||||
type: integer
|
type: integer
|
||||||
groups: [Entity, Collection]
|
groups: [Entity, Collection]
|
||||||
|
getMetaFields:
|
||||||
|
serialized_name: metaFields
|
||||||
|
exp: "object.getVisibleMetaFields()"
|
||||||
|
type: array<App\Entity\ProjectMeta>
|
||||||
|
groups: [Default]
|
||||||
|
|||||||
15
config/serializer/App/Entity.ProjectMeta.yml
Normal file
15
config/serializer/App/Entity.ProjectMeta.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
App\Entity\ProjectMeta:
|
||||||
|
exclusion_policy: All
|
||||||
|
custom_accessor_order: [name, value]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
include: false
|
||||||
|
value:
|
||||||
|
include: false
|
||||||
|
virtual_properties:
|
||||||
|
getName:
|
||||||
|
serialized_name: name
|
||||||
|
exp: "object.isVisible() ? object.getName() : null"
|
||||||
|
getValue:
|
||||||
|
serialized_name: value
|
||||||
|
exp: "object.isVisible() ? object.getValue() : null"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\Timesheet:
|
App\Entity\Timesheet:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate, tags, exported]
|
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate, tags, exported, metaFields]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -29,6 +29,8 @@ App\Entity\Timesheet:
|
|||||||
groups: [Subresource]
|
groups: [Subresource]
|
||||||
user:
|
user:
|
||||||
exclude: true
|
exclude: true
|
||||||
|
metaFields:
|
||||||
|
exclude: true
|
||||||
virtual_properties:
|
virtual_properties:
|
||||||
getBegin:
|
getBegin:
|
||||||
serialized_name: begin
|
serialized_name: begin
|
||||||
@@ -53,6 +55,11 @@ App\Entity\Timesheet:
|
|||||||
exp: "object.getUser().getId()"
|
exp: "object.getUser().getId()"
|
||||||
type: integer
|
type: integer
|
||||||
getTags:
|
getTags:
|
||||||
serialized_name: tags
|
serialized_name: tags
|
||||||
exp: "object.getTagsAsArray()"
|
exp: "object.getTagsAsArray()"
|
||||||
type: array<string>
|
type: array<string>
|
||||||
|
getMetaFields:
|
||||||
|
serialized_name: metaFields
|
||||||
|
exp: "object.getVisibleMetaFields()"
|
||||||
|
type: array<App\Entity\TimesheetMeta>
|
||||||
|
groups: [Default]
|
||||||
|
|||||||
15
config/serializer/App/Entity.TimesheetMeta.yml
Normal file
15
config/serializer/App/Entity.TimesheetMeta.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
App\Entity\TimesheetMeta:
|
||||||
|
exclusion_policy: All
|
||||||
|
custom_accessor_order: [name, value]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
include: false
|
||||||
|
value:
|
||||||
|
include: false
|
||||||
|
virtual_properties:
|
||||||
|
getName:
|
||||||
|
serialized_name: name
|
||||||
|
exp: "object.isVisible() ? object.getName() : null"
|
||||||
|
getValue:
|
||||||
|
serialized_name: value
|
||||||
|
exp: "object.isVisible() ? object.getValue() : null"
|
||||||
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
|||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Form\ActivityEditForm;
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
|
use App\Form\API\ActivityApiEditForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\Query\ActivityQuery;
|
use App\Repository\Query\ActivityQuery;
|
||||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Swagger\Annotations as SWG;
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
@@ -37,20 +39,20 @@ class ActivityController extends BaseApiController
|
|||||||
* @var ActivityRepository
|
* @var ActivityRepository
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ViewHandlerInterface
|
* @var ViewHandlerInterface
|
||||||
*/
|
*/
|
||||||
protected $viewHandler;
|
protected $viewHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ViewHandlerInterface $viewHandler
|
* @var EventDispatcherInterface
|
||||||
* @param ActivityRepository $repository
|
|
||||||
*/
|
*/
|
||||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
protected $dispatcher;
|
||||||
|
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,12 +135,18 @@ class ActivityController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
public function getAction($id)
|
public function getAction($id)
|
||||||
{
|
{
|
||||||
|
/** @var Activity $data */
|
||||||
$data = $this->repository->find($id);
|
$data = $this->repository->find($id);
|
||||||
|
|
||||||
if (null === $data) {
|
if (null === $data) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure the fields are properly setup and we know, which meta fields
|
||||||
|
// should be exposed and which not
|
||||||
|
$event = new ActivityMetaDefinitionEvent($data);
|
||||||
|
$this->dispatcher->dispatch(ActivityMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$view = new View($data, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||||
|
|
||||||
@@ -177,9 +185,7 @@ class ActivityController extends BaseApiController
|
|||||||
|
|
||||||
$activity = new Activity();
|
$activity = new Activity();
|
||||||
|
|
||||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->submit($request->request->all());
|
$form->submit($request->request->all());
|
||||||
|
|
||||||
@@ -241,9 +247,7 @@ class ActivityController extends BaseApiController
|
|||||||
throw new AccessDeniedHttpException('User cannot update activity');
|
throw new AccessDeniedHttpException('User cannot update activity');
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->setData($activity);
|
$form->setData($activity);
|
||||||
$form->submit($request->request->all(), false);
|
$form->submit($request->request->all(), false);
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
|||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Form\CustomerEditForm;
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
|
use App\Form\API\CustomerApiEditForm;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Swagger\Annotations as SWG;
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
@@ -37,20 +39,20 @@ class CustomerController extends BaseApiController
|
|||||||
* @var CustomerRepository
|
* @var CustomerRepository
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ViewHandlerInterface
|
* @var ViewHandlerInterface
|
||||||
*/
|
*/
|
||||||
protected $viewHandler;
|
protected $viewHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ViewHandlerInterface $viewHandler
|
* @var EventDispatcherInterface
|
||||||
* @param CustomerRepository $repository
|
|
||||||
*/
|
*/
|
||||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
protected $dispatcher;
|
||||||
|
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,12 +113,18 @@ class CustomerController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
public function getAction($id)
|
public function getAction($id)
|
||||||
{
|
{
|
||||||
|
/** @var Customer $data */
|
||||||
$data = $this->repository->find($id);
|
$data = $this->repository->find($id);
|
||||||
|
|
||||||
if (null === $data) {
|
if (null === $data) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure the fields are properly setup and we know, which meta fields
|
||||||
|
// should be exposed and which not
|
||||||
|
$event = new CustomerMetaDefinitionEvent($data);
|
||||||
|
$this->dispatcher->dispatch(CustomerMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$view = new View($data, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||||
|
|
||||||
@@ -155,9 +163,7 @@ class CustomerController extends BaseApiController
|
|||||||
|
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
|
|
||||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->submit($request->request->all());
|
$form->submit($request->request->all());
|
||||||
|
|
||||||
@@ -219,9 +225,7 @@ class CustomerController extends BaseApiController
|
|||||||
throw new AccessDeniedHttpException('User cannot update customer');
|
throw new AccessDeniedHttpException('User cannot update customer');
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->setData($customer);
|
$form->setData($customer);
|
||||||
$form->submit($request->request->all(), false);
|
$form->submit($request->request->all(), false);
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
|||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Form\ProjectEditForm;
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
|
use App\Form\API\ProjectApiEditForm;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\ProjectQuery;
|
use App\Repository\Query\ProjectQuery;
|
||||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Swagger\Annotations as SWG;
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
@@ -37,20 +39,20 @@ class ProjectController extends BaseApiController
|
|||||||
* @var ProjectRepository
|
* @var ProjectRepository
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ViewHandlerInterface
|
* @var ViewHandlerInterface
|
||||||
*/
|
*/
|
||||||
protected $viewHandler;
|
protected $viewHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ViewHandlerInterface $viewHandler
|
* @var EventDispatcherInterface
|
||||||
* @param ProjectRepository $repository
|
|
||||||
*/
|
*/
|
||||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
protected $dispatcher;
|
||||||
|
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,10 +119,18 @@ class ProjectController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
public function getAction($id)
|
public function getAction($id)
|
||||||
{
|
{
|
||||||
|
/** @var Project $data */
|
||||||
$data = $this->repository->find($id);
|
$data = $this->repository->find($id);
|
||||||
|
|
||||||
if (null === $data) {
|
if (null === $data) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure the fields are properly setup and we know, which meta fields
|
||||||
|
// should be exposed and which not
|
||||||
|
$event = new ProjectMetaDefinitionEvent($data);
|
||||||
|
$this->dispatcher->dispatch(ProjectMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$view = new View($data, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||||
|
|
||||||
@@ -159,9 +169,7 @@ class ProjectController extends BaseApiController
|
|||||||
|
|
||||||
$project = new Project();
|
$project = new Project();
|
||||||
|
|
||||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->submit($request->request->all());
|
$form->submit($request->request->all());
|
||||||
|
|
||||||
@@ -223,9 +231,7 @@ class ProjectController extends BaseApiController
|
|||||||
throw new AccessDeniedHttpException('User cannot update project');
|
throw new AccessDeniedHttpException('User cannot update project');
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||||
'csrf_protection' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form->setData($project);
|
$form->setData($project);
|
||||||
$form->submit($request->request->all(), false);
|
$form->submit($request->request->all(), false);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace App\API;
|
|||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Form\TimesheetEditForm;
|
use App\Form\API\TimesheetApiEditForm;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TagRepository;
|
use App\Repository\TagRepository;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
@@ -234,17 +234,18 @@ class TimesheetController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
public function getAction($id)
|
public function getAction($id)
|
||||||
{
|
{
|
||||||
$timesheet = $this->repository->find($id);
|
/** @var Timesheet $data */
|
||||||
|
$data = $this->repository->find($id);
|
||||||
|
|
||||||
if (null === $timesheet) {
|
if (null === $data) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->isGranted('view', $timesheet)) {
|
if (!$this->isGranted('view', $data)) {
|
||||||
throw new AccessDeniedHttpException('You are not allowed to view this timesheet');
|
throw new AccessDeniedHttpException('You are not allowed to view this timesheet');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view = new View($timesheet, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
@@ -284,13 +285,11 @@ class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
$mode = $this->getTrackingMode();
|
$mode = $this->getTrackingMode();
|
||||||
|
|
||||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||||
'csrf_protection' => false,
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||||
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||||
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||||
'allow_duration' => false,
|
|
||||||
'date_format' => self::DATE_FORMAT,
|
'date_format' => self::DATE_FORMAT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -366,13 +365,11 @@ class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
$mode = $this->getTrackingMode();
|
$mode = $this->getTrackingMode();
|
||||||
|
|
||||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||||
'csrf_protection' => false,
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||||
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||||
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||||
'allow_duration' => false,
|
|
||||||
'date_format' => self::DATE_FORMAT,
|
'date_format' => self::DATE_FORMAT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
use App\Form\ActivityEditForm;
|
use App\Form\ActivityEditForm;
|
||||||
use App\Form\Toolbar\ActivityToolbarForm;
|
use App\Form\Toolbar\ActivityToolbarForm;
|
||||||
use App\Form\Type\ActivityType;
|
use App\Form\Type\ActivityType;
|
||||||
@@ -19,7 +20,11 @@ use App\Repository\Query\ActivityQuery;
|
|||||||
use Doctrine\ORM\ORMException;
|
use Doctrine\ORM\ORMException;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,10 +39,15 @@ class ActivityController extends AbstractController
|
|||||||
* @var ActivityRepository
|
* @var ActivityRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $dispatcher;
|
||||||
|
|
||||||
public function __construct(ActivityRepository $repository)
|
public function __construct(ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRepository(): ActivityRepository
|
protected function getRepository(): ActivityRepository
|
||||||
@@ -52,7 +62,7 @@ class ActivityController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
@@ -88,7 +98,7 @@ class ActivityController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param Project|null $project
|
* @param Project|null $project
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request, ?Project $project = null)
|
public function createAction(Request $request, ?Project $project = null)
|
||||||
{
|
{
|
||||||
@@ -105,7 +115,7 @@ class ActivityController extends AbstractController
|
|||||||
* @Security("is_granted('budget', activity)")
|
* @Security("is_granted('budget', activity)")
|
||||||
*
|
*
|
||||||
* @param Activity $activity
|
* @param Activity $activity
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function budgetAction(Activity $activity)
|
public function budgetAction(Activity $activity)
|
||||||
{
|
{
|
||||||
@@ -121,7 +131,7 @@ class ActivityController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Activity $activity
|
* @param Activity $activity
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function editAction(Activity $activity, Request $request)
|
public function editAction(Activity $activity, Request $request)
|
||||||
{
|
{
|
||||||
@@ -134,7 +144,7 @@ class ActivityController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Activity $activity
|
* @param Activity $activity
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function deleteAction(Activity $activity, Request $request)
|
public function deleteAction(Activity $activity, Request $request)
|
||||||
{
|
{
|
||||||
@@ -193,29 +203,32 @@ class ActivityController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @param Activity $activity
|
* @param Activity $activity
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
protected function renderActivityForm(Activity $activity, Request $request)
|
protected function renderActivityForm(Activity $activity, Request $request)
|
||||||
{
|
{
|
||||||
$editForm = $this->createEditForm($activity);
|
$event = new ActivityMetaDefinitionEvent($activity);
|
||||||
|
$this->dispatcher->dispatch(ActivityMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
|
$editForm = $this->createEditForm($activity);
|
||||||
$editForm->handleRequest($request);
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
try {
|
||||||
$entityManager->persist($activity);
|
$this->getRepository()->saveActivity($activity);
|
||||||
$entityManager->flush();
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||||
|
$newActivity = new Activity();
|
||||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
$newActivity->setProject($activity->getProject());
|
||||||
$newActivity = new Activity();
|
$editForm = $this->createEditForm($newActivity);
|
||||||
$newActivity->setProject($activity->getProject());
|
$editForm->get('create_more')->setData(true);
|
||||||
$editForm = $this->createEditForm($newActivity);
|
$activity = $newActivity;
|
||||||
$editForm->get('create_more')->setData(true);
|
} else {
|
||||||
$activity = $newActivity;
|
return $this->redirectToRoute('admin_activity');
|
||||||
} else {
|
}
|
||||||
return $this->redirectToRoute('admin_activity');
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +243,7 @@ class ActivityController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ActivityQuery $query
|
* @param ActivityQuery $query
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
protected function getToolbarForm(ActivityQuery $query)
|
protected function getToolbarForm(ActivityQuery $query)
|
||||||
{
|
{
|
||||||
@@ -244,7 +257,7 @@ class ActivityController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Activity $activity
|
* @param Activity $activity
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
private function createEditForm(Activity $activity)
|
private function createEditForm(Activity $activity)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Configuration\FormConfiguration;
|
use App\Configuration\FormConfiguration;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
use App\Form\CustomerEditForm;
|
use App\Form\CustomerEditForm;
|
||||||
use App\Form\Toolbar\CustomerToolbarForm;
|
use App\Form\Toolbar\CustomerToolbarForm;
|
||||||
use App\Form\Type\CustomerType;
|
use App\Form\Type\CustomerType;
|
||||||
@@ -19,7 +20,11 @@ use App\Repository\Query\CustomerQuery;
|
|||||||
use Doctrine\ORM\ORMException;
|
use Doctrine\ORM\ORMException;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,15 +43,20 @@ class CustomerController extends AbstractController
|
|||||||
* @var FormConfiguration
|
* @var FormConfiguration
|
||||||
*/
|
*/
|
||||||
private $configuration;
|
private $configuration;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $dispatcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CustomerRepository $repository
|
* @param CustomerRepository $repository
|
||||||
* @param FormConfiguration $configuration
|
* @param FormConfiguration $configuration
|
||||||
*/
|
*/
|
||||||
public function __construct(CustomerRepository $repository, FormConfiguration $configuration)
|
public function __construct(CustomerRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->configuration = $configuration;
|
$this->configuration = $configuration;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +74,7 @@ class CustomerController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
@@ -97,7 +107,7 @@ class CustomerController extends AbstractController
|
|||||||
* @Security("is_granted('create_customer')")
|
* @Security("is_granted('create_customer')")
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request)
|
public function createAction(Request $request)
|
||||||
{
|
{
|
||||||
@@ -114,7 +124,7 @@ class CustomerController extends AbstractController
|
|||||||
* @Security("is_granted('budget', customer)")
|
* @Security("is_granted('budget', customer)")
|
||||||
*
|
*
|
||||||
* @param Customer $customer
|
* @param Customer $customer
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function budgetAction(Customer $customer)
|
public function budgetAction(Customer $customer)
|
||||||
{
|
{
|
||||||
@@ -130,7 +140,7 @@ class CustomerController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Customer $customer
|
* @param Customer $customer
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function editAction(Customer $customer, Request $request)
|
public function editAction(Customer $customer, Request $request)
|
||||||
{
|
{
|
||||||
@@ -143,7 +153,7 @@ class CustomerController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Customer $customer
|
* @param Customer $customer
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function deleteAction(Customer $customer, Request $request)
|
public function deleteAction(Customer $customer, Request $request)
|
||||||
{
|
{
|
||||||
@@ -195,22 +205,26 @@ class CustomerController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @param Customer $customer
|
* @param Customer $customer
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||||
{
|
{
|
||||||
|
$event = new CustomerMetaDefinitionEvent($customer);
|
||||||
|
$this->dispatcher->dispatch(CustomerMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$editForm = $this->createEditForm($customer);
|
$editForm = $this->createEditForm($customer);
|
||||||
|
|
||||||
$editForm->handleRequest($request);
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
try {
|
||||||
$entityManager->persist($customer);
|
$this->getRepository()->saveCustomer($customer);
|
||||||
$entityManager->flush();
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
return $this->redirectToRoute('admin_customer');
|
||||||
|
} catch (ORMException $ex) {
|
||||||
return $this->redirectToRoute('admin_customer');
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('customer/edit.html.twig', [
|
return $this->render('customer/edit.html.twig', [
|
||||||
@@ -221,7 +235,7 @@ class CustomerController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CustomerQuery $query
|
* @param CustomerQuery $query
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
protected function getToolbarForm(CustomerQuery $query)
|
protected function getToolbarForm(CustomerQuery $query)
|
||||||
{
|
{
|
||||||
@@ -235,7 +249,7 @@ class CustomerController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Customer $customer
|
* @param Customer $customer
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
private function createEditForm(Customer $customer)
|
private function createEditForm(Customer $customer)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ use App\Repository\Query\ExportQuery;
|
|||||||
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\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +78,7 @@ class ExportController extends AbstractController
|
|||||||
* @Security("is_granted('view_export')")
|
* @Security("is_granted('view_export')")
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function indexAction(Request $request)
|
public function indexAction(Request $request)
|
||||||
@@ -105,7 +107,7 @@ class ExportController extends AbstractController
|
|||||||
* @Security("is_granted('create_export')")
|
* @Security("is_granted('create_export')")
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function export(Request $request)
|
public function export(Request $request)
|
||||||
@@ -126,8 +128,6 @@ class ExportController extends AbstractController
|
|||||||
|
|
||||||
$renderer = $this->export->getRendererById($type);
|
$renderer = $this->export->getRendererById($type);
|
||||||
|
|
||||||
// this code should not be reached, as the query already filters invalid values
|
|
||||||
// when trying to call setType() with an unknown value
|
|
||||||
if (null === $renderer) {
|
if (null === $renderer) {
|
||||||
throw $this->createNotFoundException('Unknown export renderer');
|
throw $this->createNotFoundException('Unknown export renderer');
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@ class ExportController extends AbstractController
|
|||||||
* @param ExportQuery $query
|
* @param ExportQuery $query
|
||||||
* @return Timesheet[]
|
* @return Timesheet[]
|
||||||
*/
|
*/
|
||||||
protected function getEntries(ExportQuery $query)
|
protected function getEntries(ExportQuery $query): array
|
||||||
{
|
{
|
||||||
$query->getBegin()->setTime(0, 0, 0);
|
$query->getBegin()->setTime(0, 0, 0);
|
||||||
$query->getEnd()->setTime(23, 59, 59);
|
$query->getEnd()->setTime(23, 59, 59);
|
||||||
@@ -151,9 +151,9 @@ class ExportController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ExportQuery $query
|
* @param ExportQuery $query
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
protected function getToolbarForm(ExportQuery $query)
|
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', []),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
use App\Form\ProjectEditForm;
|
use App\Form\ProjectEditForm;
|
||||||
use App\Form\Toolbar\ProjectToolbarForm;
|
use App\Form\Toolbar\ProjectToolbarForm;
|
||||||
use App\Form\Type\ProjectType;
|
use App\Form\Type\ProjectType;
|
||||||
@@ -19,7 +20,11 @@ use App\Repository\Query\ProjectQuery;
|
|||||||
use Doctrine\ORM\ORMException;
|
use Doctrine\ORM\ORMException;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,10 +39,15 @@ class ProjectController extends AbstractController
|
|||||||
* @var ProjectRepository
|
* @var ProjectRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $dispatcher;
|
||||||
|
|
||||||
public function __construct(ProjectRepository $repository)
|
public function __construct(ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRepository(): ProjectRepository
|
protected function getRepository(): ProjectRepository
|
||||||
@@ -52,7 +62,7 @@ class ProjectController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
@@ -88,7 +98,7 @@ class ProjectController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param Customer|null $customer
|
* @param Customer|null $customer
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request, ?Customer $customer = null)
|
public function createAction(Request $request, ?Customer $customer = null)
|
||||||
{
|
{
|
||||||
@@ -106,7 +116,7 @@ class ProjectController extends AbstractController
|
|||||||
* @Security("is_granted('budget', project)")
|
* @Security("is_granted('budget', project)")
|
||||||
*
|
*
|
||||||
* @param Project $project
|
* @param Project $project
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function budgetAction(Project $project)
|
public function budgetAction(Project $project)
|
||||||
{
|
{
|
||||||
@@ -122,7 +132,7 @@ class ProjectController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Project $project
|
* @param Project $project
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function editAction(Project $project, Request $request)
|
public function editAction(Project $project, Request $request)
|
||||||
{
|
{
|
||||||
@@ -135,7 +145,7 @@ class ProjectController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @param Project $project
|
* @param Project $project
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function deleteAction(Project $project, Request $request)
|
public function deleteAction(Project $project, Request $request)
|
||||||
{
|
{
|
||||||
@@ -188,29 +198,32 @@ class ProjectController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @param Project $project
|
* @param Project $project
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
protected function renderProjectForm(Project $project, Request $request)
|
protected function renderProjectForm(Project $project, Request $request)
|
||||||
{
|
{
|
||||||
$editForm = $this->createEditForm($project);
|
$event = new ProjectMetaDefinitionEvent($project);
|
||||||
|
$this->dispatcher->dispatch(ProjectMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
|
$editForm = $this->createEditForm($project);
|
||||||
$editForm->handleRequest($request);
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
try {
|
||||||
$entityManager->persist($project);
|
$this->getRepository()->saveProject($project);
|
||||||
$entityManager->flush();
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||||
|
$newProject = new Project();
|
||||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
$newProject->setCustomer($project->getCustomer());
|
||||||
$newProject = new Project();
|
$editForm = $this->createEditForm($newProject);
|
||||||
$newProject->setCustomer($project->getCustomer());
|
$editForm->get('create_more')->setData(true);
|
||||||
$editForm = $this->createEditForm($newProject);
|
$project = $newProject;
|
||||||
$editForm->get('create_more')->setData(true);
|
} else {
|
||||||
$project = $newProject;
|
return $this->redirectToRoute('admin_project');
|
||||||
} else {
|
}
|
||||||
return $this->redirectToRoute('admin_project');
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +235,7 @@ class ProjectController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ProjectQuery $query
|
* @param ProjectQuery $query
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
protected function getToolbarForm(ProjectQuery $query)
|
protected function getToolbarForm(ProjectQuery $query)
|
||||||
{
|
{
|
||||||
@@ -236,7 +249,7 @@ class ProjectController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Project $project
|
* @param Project $project
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return FormInterface
|
||||||
*/
|
*/
|
||||||
private function createEditForm(Project $project)
|
private function createEditForm(Project $project)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace App\Controller;
|
|||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Tag;
|
use App\Entity\Tag;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Event\TimesheetMetaDefinitionEvent;
|
||||||
use App\Form\TimesheetEditForm;
|
use App\Form\TimesheetEditForm;
|
||||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
@@ -23,6 +24,7 @@ use App\Timesheet\TrackingMode\TrackingModeInterface;
|
|||||||
use App\Timesheet\TrackingModeService;
|
use App\Timesheet\TrackingModeService;
|
||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -45,17 +47,23 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
* @var TrackingModeService
|
* @var TrackingModeService
|
||||||
*/
|
*/
|
||||||
protected $trackingModeService;
|
protected $trackingModeService;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $dispatcher;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UserDateTimeFactory $dateTime,
|
UserDateTimeFactory $dateTime,
|
||||||
TimesheetConfiguration $configuration,
|
TimesheetConfiguration $configuration,
|
||||||
TimesheetRepository $repository,
|
TimesheetRepository $repository,
|
||||||
TrackingModeService $service
|
TrackingModeService $service,
|
||||||
|
EventDispatcherInterface $dispatcher
|
||||||
) {
|
) {
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
$this->configuration = $configuration;
|
$this->configuration = $configuration;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->trackingModeService = $service;
|
$this->trackingModeService = $service;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTrackingMode(): TrackingModeInterface
|
protected function getTrackingMode(): TrackingModeInterface
|
||||||
@@ -127,17 +135,21 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function edit(Timesheet $entry, Request $request, string $renderTemplate)
|
protected function edit(Timesheet $entry, Request $request, string $renderTemplate)
|
||||||
{
|
{
|
||||||
|
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||||
|
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||||
$editForm->handleRequest($request);
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
try {
|
||||||
$entityManager->persist($entry);
|
$this->getRepository()->save($entry);
|
||||||
$entityManager->flush();
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
|
||||||
|
} catch (\Exception $ex) {
|
||||||
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render($renderTemplate, [
|
return $this->render($renderTemplate, [
|
||||||
@@ -169,6 +181,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
$entry->setActivity($activity);
|
$entry->setActivity($activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||||
|
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||||
|
|
||||||
$mode = $this->getTrackingMode();
|
$mode = $this->getTrackingMode();
|
||||||
$mode->create($entry, $request);
|
$mode->create($entry, $request);
|
||||||
|
|
||||||
@@ -176,8 +191,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
$createForm->handleRequest($request);
|
$createForm->handleRequest($request);
|
||||||
|
|
||||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (null === $entry->getEnd()) {
|
if (null === $entry->getEnd()) {
|
||||||
$this->getRepository()->stopActiveEntries(
|
$this->getRepository()->stopActiveEntries(
|
||||||
@@ -185,15 +198,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
$this->configuration->getActiveEntriesHardLimit()
|
$this->configuration->getActiveEntriesHardLimit()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$entityManager->persist($entry);
|
$this->getRepository()->save($entry);
|
||||||
$entityManager->flush();
|
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||||
} catch (\Exception $ex) {
|
} catch (\Exception $ex) {
|
||||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render($renderTemplate, [
|
return $this->render($renderTemplate, [
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->scalarNode('mode')
|
->scalarNode('mode')
|
||||||
->defaultValue('default')
|
->defaultValue('default')
|
||||||
->validate()
|
|
||||||
->ifTrue(function ($value) {
|
|
||||||
return !in_array($value, ['default', 'duration_only', 'punch']);
|
|
||||||
})
|
|
||||||
->thenInvalid('Chosen timesheet mode is invalid, allowed values: default, duration_only, punch')
|
|
||||||
->end()
|
|
||||||
->end()
|
->end()
|
||||||
->booleanNode('markdown_content')
|
->booleanNode('markdown_content')
|
||||||
->defaultValue(false)
|
->defaultValue(false)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @ORM\Table(name="kimai2_activities")
|
* @ORM\Table(name="kimai2_activities")
|
||||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||||
*/
|
*/
|
||||||
class Activity
|
class Activity implements EntityWithMetaFields
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
@@ -73,9 +73,17 @@ class Activity
|
|||||||
use ColorTrait;
|
use ColorTrait;
|
||||||
use BudgetTrait;
|
use BudgetTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ActivityMeta[]|Collection
|
||||||
|
*
|
||||||
|
* @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->timesheets = new ArrayCollection();
|
$this->timesheets = new ArrayCollection();
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -139,6 +147,55 @@ class Activity
|
|||||||
return $this->visible;
|
return $this->visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal only here for symfony forms
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getVisibleMetaFields(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
if ($meta->isVisible()) {
|
||||||
|
$all[] = $meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
foreach ($this->meta as $field) {
|
||||||
|
if ($field->getName() === $name) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||||
|
$meta->setEntity($this);
|
||||||
|
$this->meta->add($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->merge($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
54
src/Entity/ActivityMeta.php
Normal file
54
src/Entity/ActivityMeta.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="kimai2_activities_meta",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"activity_id", "name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ActivityMeta implements MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Activity
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="meta")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $activity;
|
||||||
|
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
if (!($entity instanceof Activity)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf('Expected instanceof Activity, received "%s"', get_class($entity))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->activity = $entity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): ?EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return $this->activity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @ORM\Table(name="kimai2_customers")
|
* @ORM\Table(name="kimai2_customers")
|
||||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||||
*/
|
*/
|
||||||
class Customer
|
class Customer implements EntityWithMetaFields
|
||||||
{
|
{
|
||||||
public const DEFAULT_CURRENCY = 'EUR';
|
public const DEFAULT_CURRENCY = 'EUR';
|
||||||
|
|
||||||
@@ -154,9 +154,17 @@ class Customer
|
|||||||
use ColorTrait;
|
use ColorTrait;
|
||||||
use BudgetTrait;
|
use BudgetTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CustomerMeta[]|Collection
|
||||||
|
*
|
||||||
|
* @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->projects = new ArrayCollection();
|
$this->projects = new ArrayCollection();
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -352,6 +360,55 @@ class Customer
|
|||||||
return $this->projects;
|
return $this->projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal only here for symfony forms
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getVisibleMetaFields(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
if ($meta->isVisible()) {
|
||||||
|
$all[] = $meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
foreach ($this->meta as $field) {
|
||||||
|
if ($field->getName() === $name) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||||
|
$meta->setEntity($this);
|
||||||
|
$this->meta->add($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->merge($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
54
src/Entity/CustomerMeta.php
Normal file
54
src/Entity/CustomerMeta.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="kimai2_customers_meta",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"customer_id", "name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class CustomerMeta implements MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Customer
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="meta")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $customer;
|
||||||
|
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
if (!($entity instanceof Customer)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf('Expected instanceof Customer, received "%s"', get_class($entity))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->customer = $entity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): ?EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return $this->customer;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Entity/EntityWithMetaFields.php
Normal file
25
src/Entity/EntityWithMetaFields.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
|
||||||
|
interface EntityWithMetaFields
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @internal only here for symfony forms
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection;
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface;
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields;
|
||||||
|
}
|
||||||
136
src/Entity/MetaTableTypeInterface.php
Normal file
136
src/Entity/MetaTableTypeInterface.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
interface MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the name of this entry.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getName(): ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of this entry.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setName(string $name): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getValue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||||
|
*
|
||||||
|
* @param mixed|null $value
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setValue($value): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the linked entity.
|
||||||
|
*
|
||||||
|
* @return EntityWithMetaFields|null
|
||||||
|
*/
|
||||||
|
public function getEntity(): ?EntityWithMetaFields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the linked entity of this entry.
|
||||||
|
*
|
||||||
|
* @param EntityWithMetaFields $entity
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will merge the current object with the values from the given $meta instance.
|
||||||
|
* It should NOT update the name or value, but only the form settings.
|
||||||
|
*
|
||||||
|
* @param MetaTableTypeInterface $meta
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this field can be displayed in "public" places like API results or export.
|
||||||
|
*
|
||||||
|
* @param bool $include
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setIsVisible(bool $include): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this field can be displayed in "public" places like API results or export.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isVisible(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this field is required to be filled out in the form.
|
||||||
|
*
|
||||||
|
* @param bool $isRequired
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setIsRequired(bool $isRequired): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the form field is required.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isRequired(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form type for this field.
|
||||||
|
* If this method returns null, it will not be shown on the form.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getType(): ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the form type.
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setType(string $type): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all constraints that should be attached to the form type.
|
||||||
|
*
|
||||||
|
* @return Constraint[]
|
||||||
|
*/
|
||||||
|
public function getConstraints(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a constraint to the form type.
|
||||||
|
*
|
||||||
|
* @param Constraint $constraint
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function addConstraint(Constraint $constraint): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all constraints for the form type, overwriting all previously attached.
|
||||||
|
*
|
||||||
|
* @param Constraint[] $constraints
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setConstraints(array $constraints): MetaTableTypeInterface;
|
||||||
|
}
|
||||||
179
src/Entity/MetaTableTypeTrait.php
Normal file
179
src/Entity/MetaTableTypeTrait.php
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
trait MetaTableTypeTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue
|
||||||
|
* @ORM\Column(name="id", type="integer")
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||||
|
* @Assert\Length(min=2, max=50)
|
||||||
|
*/
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||||
|
*/
|
||||||
|
private $value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $visible = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $required = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Constraint[]
|
||||||
|
*/
|
||||||
|
private $constraints = [];
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getValue()
|
||||||
|
{
|
||||||
|
switch ($this->type) {
|
||||||
|
case CheckboxType::class:
|
||||||
|
return (bool) $this->value;
|
||||||
|
case IntegerType::class:
|
||||||
|
return (int) $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return MetaTableTypeInterface
|
||||||
|
*/
|
||||||
|
public function setValue($value): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConstraints(array $constraints): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->constraints = [];
|
||||||
|
|
||||||
|
foreach ($constraints as $constraint) {
|
||||||
|
$this->addConstraint($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addConstraint(Constraint $constraint): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->constraints[] = $constraint;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint[]
|
||||||
|
*/
|
||||||
|
public function getConstraints(): array
|
||||||
|
{
|
||||||
|
return $this->constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRequired(): bool
|
||||||
|
{
|
||||||
|
return $this->required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsRequired(bool $isRequired): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->required = $isRequired;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isVisible(): bool
|
||||||
|
{
|
||||||
|
return $this->visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsVisible(bool $visible): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this->visible = $visible;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setType($meta->getType())
|
||||||
|
->setConstraints($meta->getConstraints())
|
||||||
|
->setIsRequired($meta->isRequired())
|
||||||
|
->setIsVisible($meta->isVisible());
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @ORM\Table(name="kimai2_projects")
|
* @ORM\Table(name="kimai2_projects")
|
||||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||||
*/
|
*/
|
||||||
class Project
|
class Project implements EntityWithMetaFields
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
@@ -89,10 +89,18 @@ class Project
|
|||||||
*/
|
*/
|
||||||
private $timesheets;
|
private $timesheets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ProjectMeta[]|Collection
|
||||||
|
*
|
||||||
|
* @ORM\OneToMany(targetEntity="App\Entity\ProjectMeta", mappedBy="project", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->activities = new ArrayCollection();
|
$this->activities = new ArrayCollection();
|
||||||
$this->timesheets = new ArrayCollection();
|
$this->timesheets = new ArrayCollection();
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -190,6 +198,55 @@ class Project
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal only here for symfony forms
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getVisibleMetaFields(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
if ($meta->isVisible()) {
|
||||||
|
$all[] = $meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
foreach ($this->meta as $field) {
|
||||||
|
if ($field->getName() === $name) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||||
|
$meta->setEntity($this);
|
||||||
|
$this->meta->add($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->merge($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
54
src/Entity/ProjectMeta.php
Normal file
54
src/Entity/ProjectMeta.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="kimai2_projects_meta",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"project_id", "name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ProjectMeta implements MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Project
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="meta")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $project;
|
||||||
|
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
if (!($entity instanceof Project)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf('Expected instanceof Project, received "%s"', get_class($entity))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->project = $entity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): ?EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return $this->project;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @ORM\HasLifecycleCallbacks()
|
* @ORM\HasLifecycleCallbacks()
|
||||||
* @App\Validator\Constraints\Timesheet
|
* @App\Validator\Constraints\Timesheet
|
||||||
*/
|
*/
|
||||||
class Timesheet
|
class Timesheet implements EntityWithMetaFields
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
@@ -138,7 +138,14 @@ class Timesheet
|
|||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
protected $tags;
|
private $tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TimesheetMeta[]|Collection
|
||||||
|
*
|
||||||
|
* @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor, initializes collections
|
* Default constructor, initializes collections
|
||||||
@@ -146,6 +153,7 @@ class Timesheet
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->tags = new ArrayCollection();
|
$this->tags = new ArrayCollection();
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,6 +413,7 @@ class Timesheet
|
|||||||
/**
|
/**
|
||||||
* BE WARNED: this method should NOT be used programmatically, there is very likely no reason for it!
|
* BE WARNED: this method should NOT be used programmatically, there is very likely no reason for it!
|
||||||
*
|
*
|
||||||
|
* @internal
|
||||||
* @deprecated since it was introduced, only meant for the initial migration. Will be removed with 1.0.
|
* @deprecated since it was introduced, only meant for the initial migration. Will be removed with 1.0.
|
||||||
* @param string $timezone
|
* @param string $timezone
|
||||||
* @return Timesheet
|
* @return Timesheet
|
||||||
@@ -415,4 +424,53 @@ class Timesheet
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal only here for symfony forms
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getVisibleMetaFields(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
if ($meta->isVisible()) {
|
||||||
|
$all[] = $meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
foreach ($this->meta as $field) {
|
||||||
|
if ($field->getName() === $name) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||||
|
$meta->setEntity($this);
|
||||||
|
$this->meta->add($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->merge($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/Entity/TimesheetMeta.php
Normal file
54
src/Entity/TimesheetMeta.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="kimai2_timesheet_meta",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"timesheet_id", "name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class TimesheetMeta implements MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Timesheet
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\Timesheet", inversedBy="meta")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $timesheet;
|
||||||
|
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
if (!($entity instanceof Timesheet)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf('Expected instanceof Timesheet, received "%s"', get_class($entity))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->timesheet = $entity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): ?EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return $this->timesheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Event/ActivityMetaDefinitionEvent.php
Normal file
34
src/Event/ActivityMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event can be used, to dynamically add meta fields to activities
|
||||||
|
*/
|
||||||
|
final class ActivityMetaDefinitionEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Activity
|
||||||
|
*/
|
||||||
|
protected $entity;
|
||||||
|
|
||||||
|
public function __construct(Activity $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): Activity
|
||||||
|
{
|
||||||
|
return $this->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
/**
|
/**
|
||||||
* The ConfigureAdminMenuEvent is used for populating the administration navigation.
|
* The ConfigureAdminMenuEvent is used for populating the administration navigation.
|
||||||
*/
|
*/
|
||||||
class ConfigureAdminMenuEvent extends Event
|
final class ConfigureAdminMenuEvent extends Event
|
||||||
{
|
{
|
||||||
public const CONFIGURE = 'app.admin_menu_configure';
|
public const CONFIGURE = 'app.admin_menu_configure';
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
/**
|
/**
|
||||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||||
*/
|
*/
|
||||||
class ConfigureMainMenuEvent extends Event
|
final class ConfigureMainMenuEvent extends Event
|
||||||
{
|
{
|
||||||
public const CONFIGURE = 'app.main_menu_configure';
|
public const CONFIGURE = 'app.main_menu_configure';
|
||||||
|
|
||||||
|
|||||||
34
src/Event/CustomerMetaDefinitionEvent.php
Normal file
34
src/Event/CustomerMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event can be used, to dynamically add meta fields to customers
|
||||||
|
*/
|
||||||
|
final class CustomerMetaDefinitionEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Customer
|
||||||
|
*/
|
||||||
|
protected $entity;
|
||||||
|
|
||||||
|
public function __construct(Customer $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): Customer
|
||||||
|
{
|
||||||
|
return $this->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ use App\Entity\User;
|
|||||||
use App\Widget\WidgetContainerInterface;
|
use App\Widget\WidgetContainerInterface;
|
||||||
use Symfony\Component\EventDispatcher\Event;
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
class DashboardEvent extends Event
|
final class DashboardEvent extends Event
|
||||||
{
|
{
|
||||||
public const DASHBOARD = 'app.dashboard';
|
public const DASHBOARD = 'app.dashboard';
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ use App\Entity\User;
|
|||||||
use Symfony\Component\EventDispatcher\Event;
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event should be used, if a user profile is loaded and we full data including dynamic user preferences
|
* This event should be used, if a user profile is loaded and want to fill the dynamic user preferences
|
||||||
*/
|
*/
|
||||||
class PrepareUserEvent extends Event
|
final class PrepareUserEvent extends Event
|
||||||
{
|
{
|
||||||
public const PREPARE = 'app.prepare_user';
|
public const PREPARE = 'app.prepare_user';
|
||||||
|
|
||||||
|
|||||||
34
src/Event/ProjectMetaDefinitionEvent.php
Normal file
34
src/Event/ProjectMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event can be used, to dynamically add meta fields to projects
|
||||||
|
*/
|
||||||
|
final class ProjectMetaDefinitionEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Project
|
||||||
|
*/
|
||||||
|
protected $entity;
|
||||||
|
|
||||||
|
public function __construct(Project $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): Project
|
||||||
|
{
|
||||||
|
return $this->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ use Symfony\Component\EventDispatcher\Event;
|
|||||||
/**
|
/**
|
||||||
* This event should be used, if system configurations should be changed/added dynamically.
|
* This event should be used, if system configurations should be changed/added dynamically.
|
||||||
*/
|
*/
|
||||||
class SystemConfigurationEvent extends Event
|
final class SystemConfigurationEvent extends Event
|
||||||
{
|
{
|
||||||
public const CONFIGURE = 'app.system_configuration';
|
public const CONFIGURE = 'app.system_configuration';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace App\Event;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\EventDispatcher\Event;
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
class ThemeEvent extends Event
|
final class ThemeEvent extends Event
|
||||||
{
|
{
|
||||||
public const JAVASCRIPT = 'app.theme.javascript';
|
public const JAVASCRIPT = 'app.theme.javascript';
|
||||||
public const STYLESHEET = 'app.theme.css';
|
public const STYLESHEET = 'app.theme.css';
|
||||||
|
|||||||
34
src/Event/TimesheetMetaDefinitionEvent.php
Normal file
34
src/Event/TimesheetMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event can be used, to dynamically add meta fields to timesheets
|
||||||
|
*/
|
||||||
|
final class TimesheetMetaDefinitionEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Timesheet
|
||||||
|
*/
|
||||||
|
protected $entity;
|
||||||
|
|
||||||
|
public function __construct(Timesheet $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): Timesheet
|
||||||
|
{
|
||||||
|
return $this->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ use Symfony\Component\EventDispatcher\Event;
|
|||||||
/**
|
/**
|
||||||
* This event should be used, if further user preferences should added dynamically
|
* This event should be used, if further user preferences should added dynamically
|
||||||
*/
|
*/
|
||||||
class UserPreferenceEvent extends Event
|
final class UserPreferenceEvent extends Event
|
||||||
{
|
{
|
||||||
public const CONFIGURE = 'app.user_preferences';
|
public const CONFIGURE = 'app.user_preferences';
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,10 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
* @var DateExtensions
|
* @var DateExtensions
|
||||||
*/
|
*/
|
||||||
protected $dateExtension;
|
protected $dateExtension;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Extensions
|
* @var Extensions
|
||||||
*/
|
*/
|
||||||
protected $extension;
|
protected $extension;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
*/
|
*/
|
||||||
@@ -42,8 +40,11 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
* @param DateExtensions $dateExtension
|
* @param DateExtensions $dateExtension
|
||||||
* @param Extensions $extensions
|
* @param Extensions $extensions
|
||||||
*/
|
*/
|
||||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
public function __construct(
|
||||||
{
|
TranslatorInterface $translator,
|
||||||
|
DateExtensions $dateExtension,
|
||||||
|
Extensions $extensions
|
||||||
|
) {
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->dateExtension = $dateExtension;
|
$this->dateExtension = $dateExtension;
|
||||||
$this->extension = $extensions;
|
$this->extension = $extensions;
|
||||||
@@ -97,6 +98,15 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
*/
|
*/
|
||||||
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
|
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
|
||||||
{
|
{
|
||||||
|
$publicMetaFields = [];
|
||||||
|
foreach ($timesheets as $timesheet) {
|
||||||
|
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||||
|
$publicMetaFields[] = $metaField->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$publicMetaFields = array_unique($publicMetaFields);
|
||||||
|
|
||||||
$spreadsheet = new Spreadsheet();
|
$spreadsheet = new Spreadsheet();
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
@@ -112,10 +122,13 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
|
||||||
|
foreach ($publicMetaFields as $metaFieldName) {
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaFieldName));
|
||||||
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||||
|
|
||||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||||
|
|
||||||
@@ -148,21 +161,32 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||||
|
foreach ($publicMetaFields as $metaFieldName) {
|
||||||
|
$metaField = $timesheet->getMetaField($metaFieldName);
|
||||||
|
$metaFieldValue = '';
|
||||||
|
if (null !== $metaField && $metaField->isVisible()) {
|
||||||
|
$metaFieldValue = $metaField->getValue();
|
||||||
|
}
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
||||||
|
}
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||||
|
|
||||||
$entryHeaderRow++;
|
$entryHeaderRow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sheet->setCellValueByColumnAndRow(12, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
$cellDurationTotal = $recordsHeaderColumn - 1;
|
||||||
$sheet->setCellValueByColumnAndRow(13, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
$cellRateTotal = $recordsHeaderColumn;
|
||||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
|
||||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
|
||||||
|
|
||||||
$sheet->getCellByColumnAndRow(13, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$sheet->setCellValueByColumnAndRow($cellDurationTotal, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||||
$sheet->getCellByColumnAndRow(13, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
$sheet->setCellValueByColumnAndRow($cellRateTotal, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||||
|
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
|
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||||
|
|
||||||
|
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
|
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||||
|
|
||||||
return $spreadsheet;
|
return $spreadsheet;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use App\Repository\Query\TimesheetQuery;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
class HtmlRenderer implements RendererInterface
|
final class HtmlRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
use RendererTrait;
|
use RendererTrait;
|
||||||
|
|
||||||
@@ -24,9 +24,6 @@ class HtmlRenderer implements RendererInterface
|
|||||||
*/
|
*/
|
||||||
protected $twig;
|
protected $twig;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Environment $twig
|
|
||||||
*/
|
|
||||||
public function __construct(Environment $twig)
|
public function __construct(Environment $twig)
|
||||||
{
|
{
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
@@ -42,9 +39,17 @@ class HtmlRenderer implements RendererInterface
|
|||||||
*/
|
*/
|
||||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
{
|
{
|
||||||
|
$publicMetaFields = [];
|
||||||
|
foreach ($timesheets as $timesheet) {
|
||||||
|
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||||
|
$publicMetaFields[] = $metaField->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$content = $this->twig->render('export/renderer/default.html.twig', [
|
$content = $this->twig->render('export/renderer/default.html.twig', [
|
||||||
'entries' => $timesheets,
|
'entries' => $timesheets,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
|
'metaFields' => array_unique($publicMetaFields),
|
||||||
'summaries' => $this->calculateSummary($timesheets),
|
'summaries' => $this->calculateSummary($timesheets),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
class PDFRenderer implements RendererInterface
|
final class PDFRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
use RendererTrait;
|
use RendererTrait;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
40
src/Form/API/ActivityApiEditForm.php
Normal file
40
src/Form/API/ActivityApiEditForm.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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\Form\API;
|
||||||
|
|
||||||
|
use App\Form\ActivityEditForm;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class ActivityApiEditForm extends ActivityEditForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
parent::buildForm($builder, $options);
|
||||||
|
|
||||||
|
$builder->remove('metaFields');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'create_more' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Form/API/CustomerApiEditForm.php
Normal file
39
src/Form/API/CustomerApiEditForm.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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\Form\API;
|
||||||
|
|
||||||
|
use App\Form\CustomerEditForm;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class CustomerApiEditForm extends CustomerEditForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
parent::buildForm($builder, $options);
|
||||||
|
|
||||||
|
$builder->remove('metaFields');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'csrf_protection' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Form/API/ProjectApiEditForm.php
Normal file
40
src/Form/API/ProjectApiEditForm.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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\Form\API;
|
||||||
|
|
||||||
|
use App\Form\ProjectEditForm;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class ProjectApiEditForm extends ProjectEditForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
parent::buildForm($builder, $options);
|
||||||
|
|
||||||
|
$builder->remove('metaFields');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'create_more' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Form/API/TimesheetApiEditForm.php
Normal file
37
src/Form/API/TimesheetApiEditForm.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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\Form\API;
|
||||||
|
|
||||||
|
use App\Form\TimesheetEditForm;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TimesheetApiEditForm extends TimesheetEditForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
parent::buildForm($builder, $options);
|
||||||
|
|
||||||
|
$builder->remove('metaFields');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'allow_duration' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use App\Form\Type\ColorPickerType;
|
|||||||
use App\Form\Type\DurationType;
|
use App\Form\Type\DurationType;
|
||||||
use App\Form\Type\FixedRateType;
|
use App\Form\Type\FixedRateType;
|
||||||
use App\Form\Type\HourlyRateType;
|
use App\Form\Type\HourlyRateType;
|
||||||
|
use App\Form\Type\MetaFieldsCollectionType;
|
||||||
use App\Form\Type\YesNoType;
|
use App\Form\Type\YesNoType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||||
@@ -47,6 +48,8 @@ trait EntityFormTrait
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->add('visible', YesNoType::class, [
|
->add('visible', YesNoType::class, [
|
||||||
'label' => 'label.visible',
|
'label' => 'label.visible',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Form\Type\DateTimePickerType;
|
|||||||
use App\Form\Type\DurationType;
|
use App\Form\Type\DurationType;
|
||||||
use App\Form\Type\FixedRateType;
|
use App\Form\Type\FixedRateType;
|
||||||
use App\Form\Type\HourlyRateType;
|
use App\Form\Type\HourlyRateType;
|
||||||
|
use App\Form\Type\MetaFieldsCollectionType;
|
||||||
use App\Form\Type\ProjectType;
|
use App\Form\Type\ProjectType;
|
||||||
use App\Form\Type\TagsInputType;
|
use App\Form\Type\TagsInputType;
|
||||||
use App\Form\Type\UserType;
|
use App\Form\Type\UserType;
|
||||||
@@ -134,6 +135,8 @@ class TimesheetEditForm extends AbstractType
|
|||||||
$this->addTags($builder);
|
$this->addTags($builder);
|
||||||
$this->addRates($builder, $currency, $options);
|
$this->addRates($builder, $currency, $options);
|
||||||
$this->addUser($builder, $options);
|
$this->addUser($builder, $options);
|
||||||
|
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||||
|
|
||||||
$this->addExported($builder, $options);
|
$this->addExported($builder, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/Form/Type/EntityMetaDefinitionType.php
Normal file
60
src/Form/Type/EntityMetaDefinitionType.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to edit entity meta field.
|
||||||
|
*/
|
||||||
|
class EntityMetaDefinitionType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param FormBuilderInterface $builder
|
||||||
|
* @param array $options
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->addEventListener(
|
||||||
|
FormEvents::PRE_SET_DATA,
|
||||||
|
function (FormEvent $event) {
|
||||||
|
/** @var MetaTableTypeInterface $definition */
|
||||||
|
$definition = $event->getData();
|
||||||
|
|
||||||
|
if (!($definition instanceof MetaTableTypeInterface)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevents unconfigured values from showing up in the form
|
||||||
|
if (null === $definition->getType()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->getForm()->add('value', $definition->getType(), [
|
||||||
|
'label' => $definition->getName(),
|
||||||
|
'constraints' => $definition->getConstraints(),
|
||||||
|
'required' => $definition->isRequired(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => MetaTableTypeInterface::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Form/Type/MetaFieldsCollectionType.php
Normal file
39
src/Form/Type/MetaFieldsCollectionType.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to edit entity meta fields.
|
||||||
|
*/
|
||||||
|
class MetaFieldsCollectionType extends AbstractType
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'entry_type' => EntityMetaDefinitionType::class,
|
||||||
|
'entry_options' => ['label' => false],
|
||||||
|
'allow_add' => false,
|
||||||
|
'allow_delete' => false,
|
||||||
|
'label' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return CollectionType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ trait RendererTrait
|
|||||||
{
|
{
|
||||||
$customer = $model->getCustomer();
|
$customer = $model->getCustomer();
|
||||||
$project = $model->getQuery()->getProject();
|
$project = $model->getQuery()->getProject();
|
||||||
|
$activity = $model->getQuery()->getActivity();
|
||||||
$currency = $model->getCalculator()->getCurrency();
|
$currency = $model->getCalculator()->getCurrency();
|
||||||
|
|
||||||
$values = [
|
$values = [
|
||||||
@@ -108,6 +109,20 @@ trait RendererTrait
|
|||||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (null !== $activity) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'activity.id' => $activity->getId(),
|
||||||
|
'activity.name' => $activity->getName(),
|
||||||
|
'activity.comment' => $activity->getComment(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (null !== $project) {
|
if (null !== $project) {
|
||||||
$values = array_merge($values, [
|
$values = array_merge($values, [
|
||||||
'project.id' => $project->getId(),
|
'project.id' => $project->getId(),
|
||||||
@@ -115,6 +130,12 @@ trait RendererTrait
|
|||||||
'project.comment' => $project->getComment(),
|
'project.comment' => $project->getComment(),
|
||||||
'project.order_number' => $project->getOrderNumber(),
|
'project.order_number' => $project->getOrderNumber(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $customer) {
|
if (null !== $customer) {
|
||||||
@@ -129,6 +150,12 @@ trait RendererTrait
|
|||||||
'customer.homepage' => $customer->getHomepage(),
|
'customer.homepage' => $customer->getHomepage(),
|
||||||
'customer.comment' => $customer->getComment(),
|
'customer.comment' => $customer->getComment(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $values;
|
return $values;
|
||||||
@@ -169,7 +196,7 @@ trait RendererTrait
|
|||||||
$begin = $timesheet->getBegin();
|
$begin = $timesheet->getBegin();
|
||||||
$end = $timesheet->getEnd();
|
$end = $timesheet->getEnd();
|
||||||
|
|
||||||
return [
|
$values = [
|
||||||
'entry.row' => '',
|
'entry.row' => '',
|
||||||
'entry.description' => $description,
|
'entry.description' => $description,
|
||||||
'entry.amount' => $amount,
|
'entry.amount' => $amount,
|
||||||
@@ -196,6 +223,14 @@ trait RendererTrait
|
|||||||
'entry.customer' => $customer->getName(),
|
'entry.customer' => $customer->getName(),
|
||||||
'entry.customer_id' => $customer->getId(),
|
'entry.customer_id' => $customer->getId(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'entry.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
83
src/Migrations/Version20190617100845.php
Normal file
83
src/Migrations/Version20190617100845.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates meta tables to store custom fields for entities
|
||||||
|
*
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
final class Version20190617100845 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Creates meta tables to store custom fields for entities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$timesheetMeta = $schema->createTable('kimai2_timesheet_meta');
|
||||||
|
$timesheetMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$timesheetMeta->addColumn('timesheet_id', 'integer', ['notnull' => true]);
|
||||||
|
$timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||||
|
$timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||||
|
$timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||||
|
$timesheetMeta->setPrimaryKey(['id']);
|
||||||
|
$timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE');
|
||||||
|
$timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06');
|
||||||
|
$timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE');
|
||||||
|
|
||||||
|
$customerMeta = $schema->createTable('kimai2_customers_meta');
|
||||||
|
$customerMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$customerMeta->addColumn('customer_id', 'integer', ['notnull' => true]);
|
||||||
|
$customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||||
|
$customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||||
|
$customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||||
|
$customerMeta->setPrimaryKey(['id']);
|
||||||
|
$customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3');
|
||||||
|
$customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06');
|
||||||
|
$customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3');
|
||||||
|
|
||||||
|
$projectMeta = $schema->createTable('kimai2_projects_meta');
|
||||||
|
$projectMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$projectMeta->addColumn('project_id', 'integer', ['notnull' => true]);
|
||||||
|
$projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||||
|
$projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||||
|
$projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||||
|
$projectMeta->setPrimaryKey(['id']);
|
||||||
|
$projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C');
|
||||||
|
$projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06');
|
||||||
|
$projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C');
|
||||||
|
|
||||||
|
$activityMeta = $schema->createTable('kimai2_activities_meta');
|
||||||
|
$activityMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$activityMeta->addColumn('activity_id', 'integer', ['notnull' => true]);
|
||||||
|
$activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||||
|
$activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||||
|
$activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||||
|
$activityMeta->setPrimaryKey(['id']);
|
||||||
|
$activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096');
|
||||||
|
$activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06');
|
||||||
|
$activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$schema->dropTable('kimai2_timesheet_meta');
|
||||||
|
$schema->dropTable('kimai2_customers_meta');
|
||||||
|
$schema->dropTable('kimai2_projects_meta');
|
||||||
|
$schema->dropTable('kimai2_activities_meta');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,18 @@ use Pagerfanta\Pagerfanta;
|
|||||||
|
|
||||||
class ActivityRepository extends AbstractRepository
|
class ActivityRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param Activity $activity
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function saveActivity(Activity $activity)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->persist($activity);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return null|Activity
|
* @return null|Activity
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ use Pagerfanta\Pagerfanta;
|
|||||||
|
|
||||||
class CustomerRepository extends AbstractRepository
|
class CustomerRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param Customer $customer
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function saveCustomer(Customer $customer)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->persist($customer);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return null|Customer
|
* @return null|Customer
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ final class TimesheetIdLoader implements LoaderInterface
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
/*
|
|
||||||
$qb = $em->createQueryBuilder();
|
$qb = $em->createQueryBuilder();
|
||||||
$qb->select('PARTIAL t.{id}', 'meta')
|
$qb->select('PARTIAL t.{id}', 'meta')
|
||||||
->from(Timesheet::class, 't')
|
->from(Timesheet::class, 't')
|
||||||
@@ -90,6 +89,5 @@ final class TimesheetIdLoader implements LoaderInterface
|
|||||||
->andWhere($qb->expr()->in('t.id', $ids))
|
->andWhere($qb->expr()->in('t.id', $ids))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ use Pagerfanta\Pagerfanta;
|
|||||||
*/
|
*/
|
||||||
class ProjectRepository extends AbstractRepository
|
class ProjectRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param Project $project
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function saveProject(Project $project)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->persist($project);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return null|Project
|
* @return null|Project
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class DurationFixedStartMode implements TrackingModeInterface
|
class DurationFixedBeginMode implements TrackingModeInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
@@ -62,7 +62,7 @@ class DurationFixedStartMode implements TrackingModeInterface
|
|||||||
|
|
||||||
public function getId(): string
|
public function getId(): string
|
||||||
{
|
{
|
||||||
return 'duration_fixed_start';
|
return 'duration_fixed_begin';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canSeeBeginAndEndTimes(): bool
|
public function canSeeBeginAndEndTimes(): bool
|
||||||
@@ -11,7 +11,7 @@ namespace App\Timesheet;
|
|||||||
|
|
||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Timesheet\TrackingMode\DefaultMode;
|
use App\Timesheet\TrackingMode\DefaultMode;
|
||||||
use App\Timesheet\TrackingMode\DurationFixedStartMode;
|
use App\Timesheet\TrackingMode\DurationFixedBeginMode;
|
||||||
use App\Timesheet\TrackingMode\DurationOnlyMode;
|
use App\Timesheet\TrackingMode\DurationOnlyMode;
|
||||||
use App\Timesheet\TrackingMode\PunchInOutMode;
|
use App\Timesheet\TrackingMode\PunchInOutMode;
|
||||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
@@ -43,7 +43,7 @@ class TrackingModeService
|
|||||||
new DefaultMode($this->dateTime, $this->configuration),
|
new DefaultMode($this->dateTime, $this->configuration),
|
||||||
new PunchInOutMode(),
|
new PunchInOutMode(),
|
||||||
new DurationOnlyMode($this->dateTime, $this->configuration),
|
new DurationOnlyMode($this->dateTime, $this->configuration),
|
||||||
new DurationFixedStartMode($this->dateTime, $this->configuration),
|
new DurationFixedBeginMode($this->dateTime, $this->configuration),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% embed 'embeds/modal.html.twig' %}
|
{% embed 'embeds/modal.html.twig' %}
|
||||||
|
{% set used = used|default(false) %}
|
||||||
{% block modal_id %}form_modal{% endblock %}
|
{% block modal_id %}form_modal{% endblock %}
|
||||||
{% block modal_class %}{% if used %}modal-danger{% endif %}{% endblock %}
|
{% block modal_class %}{% if used %}modal-danger{% endif %}{% endblock %}
|
||||||
{% block modal_before %}{{ form_start(form) }}{% endblock %}
|
{% block modal_before %}{{ form_start(form) }}{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% extends 'export/layout.html.twig' %}
|
{% extends 'export/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% set columnTitles = {} %}
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'date': true,
|
'date': true,
|
||||||
'username': false,
|
'username': false,
|
||||||
@@ -8,13 +9,21 @@
|
|||||||
'project': true,
|
'project': true,
|
||||||
'activity': true,
|
'activity': true,
|
||||||
'description': false,
|
'description': false,
|
||||||
'tags': false,
|
|
||||||
'exported': false,
|
'exported': false,
|
||||||
|
'tags': false,
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% for id, metaField in metaFields %}
|
||||||
|
{% set columns = columns|merge({ (metaField): true}) %}
|
||||||
|
{% set columnTitles = columnTitles|merge({ (metaField): (metaField)}) %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% set columns = columns|merge({
|
||||||
'hourlyRate': false,
|
'hourlyRate': false,
|
||||||
'fixedRate': false,
|
'fixedRate': false,
|
||||||
'duration': 'label.duration',
|
'duration': 'label.duration',
|
||||||
'rate': 'label.rate',
|
'rate': 'label.rate',
|
||||||
} %}
|
}) %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@@ -97,7 +106,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="records-column-{{ columnId }}">
|
<label class="control-label" for="records-column-{{ columnId }}">
|
||||||
<input type="checkbox" class="column-visibility-changer" id="records-column-{{ columnId }}" name="{{ columnId }}" {% if visibility %}checked="checked"{% endif %}>
|
<input type="checkbox" class="column-visibility-changer" id="records-column-{{ columnId }}" name="{{ columnId }}" {% if visibility %}checked="checked"{% endif %}>
|
||||||
{{ ('label.'~columnId)|trans }}
|
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -229,7 +238,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for columnId, visibility in columns %}
|
{% for columnId, visibility in columns %}
|
||||||
<th class="column column-{{ columnId }}" {% if not visibility %}style="display: none"{% endif %}>{{ ('label.'~columnId)|trans }}</th>
|
<th class="column column-{{ columnId }}" {% if not visibility %}style="display: none"{% endif %}>
|
||||||
|
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
|
||||||
|
</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -268,11 +279,6 @@
|
|||||||
{{ entry.description|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="column-tags" {% if not columns.tags %}style="display: none"{% endif %}>
|
|
||||||
{% if entry.tags is not empty %}
|
|
||||||
{{ entry.tagsAsArray|join(', ') }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="column-exported" {% if not columns.exported %}style="display: none"{% endif %}>
|
<td class="column-exported" {% if not columns.exported %}style="display: none"{% endif %}>
|
||||||
{% if entry.exported %}
|
{% if entry.exported %}
|
||||||
{{ 'entryState.exported'|trans }}
|
{{ 'entryState.exported'|trans }}
|
||||||
@@ -280,6 +286,19 @@
|
|||||||
{{ 'entryState.not_exported'|trans }}
|
{{ 'entryState.not_exported'|trans }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="column-tags" {% if not columns.tags %}style="display: none"{% endif %}>
|
||||||
|
{% if entry.tags is not empty %}
|
||||||
|
{{ entry.tagsAsArray|join(', ') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% for id, metaFieldName in metaFields %}
|
||||||
|
<td class="column-{{ metaFieldName }} text-nowrap" {% if not columns[metaFieldName] %}style="display: none"{% endif %}>
|
||||||
|
{% set metaField = entry.metaField(metaFieldName) %}
|
||||||
|
{% if not metaField is null and metaField.visible %}
|
||||||
|
{{ metaField.value }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
<td class="column-hourlyRate text-nowrap" {% if not columns.hourlyRate %}style="display: none"{% endif %}>
|
<td class="column-hourlyRate text-nowrap" {% if not columns.hourlyRate %}style="display: none"{% endif %}>
|
||||||
{{ entry.hourlyRate|money(entry.project.customer.currency) }}
|
{{ entry.hourlyRate|money(entry.project.customer.currency) }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -220,10 +220,12 @@ 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'];
|
$expectedKeys = ['id', 'name', 'visible', 'project', 'hourlyRate', 'fixedRate', 'color', 'metaFields'];
|
||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge($expectedKeys, ['comment', 'budget', 'timeBudget']);
|
$expectedKeys = array_merge($expectedKeys, [
|
||||||
|
'comment', 'budget', 'timeBudget'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$actual = array_keys($result);
|
$actual = array_keys($result);
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ 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'];
|
$expectedKeys = ['id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields'];
|
||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge($expectedKeys, [
|
$expectedKeys = array_merge($expectedKeys, [
|
||||||
|
|||||||
@@ -212,14 +212,13 @@ 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'
|
'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields'
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge(
|
$expectedKeys = array_merge($expectedKeys, [
|
||||||
$expectedKeys,
|
'comment', 'budget', 'timeBudget', 'orderNumber'
|
||||||
['comment', 'budget', 'timeBudget', 'orderNumber']
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$actual = array_keys($result);
|
$actual = array_keys($result);
|
||||||
|
|||||||
@@ -819,7 +819,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
protected function assertDefaultStructure(array $result, $full = true)
|
protected function assertDefaultStructure(array $result, $full = true)
|
||||||
{
|
{
|
||||||
$expectedKeys = [
|
$expectedKeys = [
|
||||||
'id', 'begin', 'end', 'duration', 'description', 'rate', 'activity', 'project', 'tags', 'user'
|
'id', 'begin', 'end', 'duration', 'description', 'rate', 'activity', 'project', 'tags', 'user', 'metaFields'
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use App\Entity\User;
|
|||||||
use App\Tests\DataFixtures\ActivityFixtures;
|
use App\Tests\DataFixtures\ActivityFixtures;
|
||||||
use App\Tests\DataFixtures\ProjectFixtures;
|
use App\Tests\DataFixtures\ProjectFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
|
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,18 @@ class ActivityControllerTest extends ControllerBaseTest
|
|||||||
$this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
|
$this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateActionShowsMetaFields()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$client->getContainer()->get('event_dispatcher')->addSubscriber(new ActivityTestMetaFieldSubscriberMock());
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||||
|
$this->assertTrue($form->has('activity_edit_form[metaFields][0][value]'));
|
||||||
|
$this->assertFalse($form->has('activity_edit_form[metaFields][1][value]'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreateActionWithCreateMore()
|
public function testCreateActionWithCreateMore()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Tests\DataFixtures\CustomerFixtures;
|
use App\Tests\DataFixtures\CustomerFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
|
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,18 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasFlashSuccess($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateActionShowsMetaFields()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$client->getContainer()->get('event_dispatcher')->addSubscriber(new CustomerTestMetaFieldSubscriberMock());
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/customer/create');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=customer_edit_form]')->form();
|
||||||
|
$this->assertTrue($form->has('customer_edit_form[metaFields][0][value]'));
|
||||||
|
$this->assertFalse($form->has('customer_edit_form[metaFields][1][value]'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testEditAction()
|
public function testEditAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use App\Entity\User;
|
|||||||
use App\Tests\DataFixtures\CustomerFixtures;
|
use App\Tests\DataFixtures\CustomerFixtures;
|
||||||
use App\Tests\DataFixtures\ProjectFixtures;
|
use App\Tests\DataFixtures\ProjectFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
|
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +71,18 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasFlashSuccess($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateActionShowsMetaFields()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$client->getContainer()->get('event_dispatcher')->addSubscriber(new ProjectTestMetaFieldSubscriberMock());
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/project/create');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=project_edit_form]')->form();
|
||||||
|
$this->assertTrue($form->has('project_edit_form[metaFields][0][value]'));
|
||||||
|
$this->assertFalse($form->has('project_edit_form[metaFields][1][value]'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreateActionWithCreateMore()
|
public function testCreateActionWithCreateMore()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Form\Type\DateRangeType;
|
use App\Form\Type\DateRangeType;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
|
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -144,6 +145,18 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertNull($timesheet->getFixedRate());
|
$this->assertNull($timesheet->getFixedRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateActionShowsMetaFields()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser();
|
||||||
|
$client->getContainer()->get('event_dispatcher')->addSubscriber(new TimesheetTestMetaFieldSubscriberMock());
|
||||||
|
$this->request($client, '/timesheet/create');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||||
|
$this->assertTrue($form->has('timesheet_edit_form[metaFields][0][value]'));
|
||||||
|
$this->assertFalse($form->has('timesheet_edit_form[metaFields][1][value]'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreateActionDoesNotShowRateFieldsForUser()
|
public function testCreateActionDoesNotShowRateFieldsForUser()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser();
|
$client = $this->getClientForAuthenticatedUser();
|
||||||
|
|||||||
114
tests/Entity/AbstractMetaEntityTest.php
Normal file
114
tests/Entity/AbstractMetaEntityTest.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
use App\Form\Type\DateTimePickerType;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
|
|
||||||
|
abstract class AbstractMetaEntityTest extends TestCase
|
||||||
|
{
|
||||||
|
abstract protected function getEntity(): EntityWithMetaFields;
|
||||||
|
|
||||||
|
abstract protected function getMetaEntity(): MetaTableTypeInterface;
|
||||||
|
|
||||||
|
public function testDefaultValues()
|
||||||
|
{
|
||||||
|
$sut = $this->getMetaEntity();
|
||||||
|
self::assertNull($sut->getName());
|
||||||
|
self::assertNull($sut->getType());
|
||||||
|
self::assertNull($sut->getValue());
|
||||||
|
self::assertNull($sut->getEntity());
|
||||||
|
self::assertIsArray($sut->getConstraints());
|
||||||
|
self::assertEmpty($sut->getConstraints());
|
||||||
|
self::assertFalse($sut->isVisible());
|
||||||
|
self::assertFalse($sut->isRequired());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetterAndGetter()
|
||||||
|
{
|
||||||
|
$sut = $this->getMetaEntity();
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setName('foo-bar'));
|
||||||
|
self::assertEquals('foo-bar', $sut->getName());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setisVisible(true));
|
||||||
|
self::assertTrue($sut->isVisible());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setIsRequired(true));
|
||||||
|
self::assertTrue($sut->isRequired());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setValue('hello world'));
|
||||||
|
self::assertEquals('hello world', $sut->getValue());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setValue(956.32));
|
||||||
|
self::assertEquals(956.32, $sut->getValue());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setType(DateTimePickerType::class));
|
||||||
|
self::assertEquals(DateTimePickerType::class, $sut->getType());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->addConstraint(new Length(['max' => 10])));
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->addConstraint(new NotNull([])));
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->addConstraint(new NotBlank([])));
|
||||||
|
self::assertCount(3, $sut->getConstraints());
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setConstraints([new Length(['min' => 2])]));
|
||||||
|
self::assertCount(1, $sut->getConstraints());
|
||||||
|
|
||||||
|
$entity = $this->getEntity();
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setEntity($entity));
|
||||||
|
self::assertSame($entity, $sut->getEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMerge()
|
||||||
|
{
|
||||||
|
$entity1 = $this->getEntity();
|
||||||
|
$entity2 = $this->getEntity();
|
||||||
|
$meta1 = $this->getMetaEntity();
|
||||||
|
$meta1
|
||||||
|
->setName('foo')
|
||||||
|
->setValue('bar')
|
||||||
|
->setType('blub')
|
||||||
|
->setEntity($entity1)
|
||||||
|
->setConstraints([new NotNull()])
|
||||||
|
;
|
||||||
|
self::assertEquals('foo', $meta1->getName());
|
||||||
|
self::assertEquals('bar', $meta1->getValue());
|
||||||
|
self::assertEquals('blub', $meta1->getType());
|
||||||
|
self::assertFalse($meta1->isRequired());
|
||||||
|
self::assertFalse($meta1->isVisible());
|
||||||
|
self::assertSame($entity1, $meta1->getEntity());
|
||||||
|
self::assertCount(1, $meta1->getConstraints());
|
||||||
|
|
||||||
|
$meta2 = $this->getMetaEntity();
|
||||||
|
$meta2
|
||||||
|
->setName('foo2')
|
||||||
|
->setValue('bar2')
|
||||||
|
->setType('blub2')
|
||||||
|
->setEntity($entity2)
|
||||||
|
->setIsRequired(true)
|
||||||
|
->setisVisible(true)
|
||||||
|
->setConstraints([new NotBlank(), new Length(['min' => 1])])
|
||||||
|
;
|
||||||
|
|
||||||
|
self::assertInstanceOf(MetaTableTypeInterface::class, $meta1->merge($meta2));
|
||||||
|
|
||||||
|
self::assertEquals('foo', $meta1->getName());
|
||||||
|
self::assertEquals('bar', $meta1->getValue());
|
||||||
|
self::assertEquals('blub2', $meta1->getType());
|
||||||
|
self::assertTrue($meta1->isRequired());
|
||||||
|
self::assertTrue($meta1->isVisible());
|
||||||
|
self::assertSame($entity1, $meta1->getEntity());
|
||||||
|
self::assertCount(2, $meta1->getConstraints());
|
||||||
|
}
|
||||||
|
}
|
||||||
42
tests/Entity/ActivityMetaTest.php
Normal file
42
tests/Entity/ActivityMetaTest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\ActivityMeta;
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\ActivityMeta
|
||||||
|
*/
|
||||||
|
class ActivityMetaTest extends AbstractMetaEntityTest
|
||||||
|
{
|
||||||
|
protected function getEntity(): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return new Activity();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMetaEntity(): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
return new ActivityMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Expected instanceof Activity, received "App\Entity\Timesheet"
|
||||||
|
*/
|
||||||
|
public function testSetEntityThrowsException()
|
||||||
|
{
|
||||||
|
$sut = new ActivityMeta();
|
||||||
|
$sut->setEntity(new Timesheet());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
namespace App\Tests\Entity;
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\ActivityMeta;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +33,9 @@ class ActivityTest extends TestCase
|
|||||||
$this->assertNull($sut->getColor());
|
$this->assertNull($sut->getColor());
|
||||||
$this->assertEquals(0.0, $sut->getBudget());
|
$this->assertEquals(0.0, $sut->getBudget());
|
||||||
$this->assertEquals(0, $sut->getTimeBudget());
|
$this->assertEquals(0, $sut->getTimeBudget());
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter()
|
||||||
@@ -61,4 +66,31 @@ class ActivityTest extends TestCase
|
|||||||
$this->assertInstanceOf(Activity::class, $sut->setTimeBudget(937321));
|
$this->assertInstanceOf(Activity::class, $sut->setTimeBudget(937321));
|
||||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMetaFields()
|
||||||
|
{
|
||||||
|
$sut = new Activity();
|
||||||
|
$meta = new ActivityMeta();
|
||||||
|
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||||
|
$this->assertInstanceOf(Activity::class, $sut->setMetaField($meta));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test', $result->getType());
|
||||||
|
|
||||||
|
$meta2 = new ActivityMeta();
|
||||||
|
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||||
|
$this->assertInstanceOf(Activity::class, $sut->setMetaField($meta2));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||||
|
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test2', $result->getType());
|
||||||
|
|
||||||
|
$sut->setMetaField((new ActivityMeta())->setName('blub')->setIsVisible(true));
|
||||||
|
$sut->setMetaField((new ActivityMeta())->setName('blab')->setIsVisible(true));
|
||||||
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
tests/Entity/CustomerMetaTest.php
Normal file
42
tests/Entity/CustomerMetaTest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\CustomerMeta;
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\CustomerMeta
|
||||||
|
*/
|
||||||
|
class CustomerMetaTest extends AbstractMetaEntityTest
|
||||||
|
{
|
||||||
|
protected function getEntity(): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return new Customer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMetaEntity(): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
return new CustomerMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Expected instanceof Customer, received "App\Entity\Activity"
|
||||||
|
*/
|
||||||
|
public function testSetEntityThrowsException()
|
||||||
|
{
|
||||||
|
$sut = new CustomerMeta();
|
||||||
|
$sut->setEntity(new Activity());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
namespace App\Tests\Entity;
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\CustomerMeta;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +48,9 @@ class CustomerTest extends TestCase
|
|||||||
$this->assertNull($sut->getColor());
|
$this->assertNull($sut->getColor());
|
||||||
$this->assertEquals(0.0, $sut->getBudget());
|
$this->assertEquals(0.0, $sut->getBudget());
|
||||||
$this->assertEquals(0, $sut->getTimeBudget());
|
$this->assertEquals(0, $sut->getTimeBudget());
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter()
|
||||||
@@ -97,4 +102,31 @@ class CustomerTest extends TestCase
|
|||||||
$this->assertInstanceOf(Customer::class, $sut->setTimeBudget(937321));
|
$this->assertInstanceOf(Customer::class, $sut->setTimeBudget(937321));
|
||||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMetaFields()
|
||||||
|
{
|
||||||
|
$sut = new Customer();
|
||||||
|
$meta = new CustomerMeta();
|
||||||
|
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||||
|
$this->assertInstanceOf(Customer::class, $sut->setMetaField($meta));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test', $result->getType());
|
||||||
|
|
||||||
|
$meta2 = new CustomerMeta();
|
||||||
|
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||||
|
$this->assertInstanceOf(Customer::class, $sut->setMetaField($meta2));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||||
|
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test2', $result->getType());
|
||||||
|
|
||||||
|
$sut->setMetaField((new CustomerMeta())->setName('blub')->setIsVisible(true));
|
||||||
|
$sut->setMetaField((new CustomerMeta())->setName('blab')->setIsVisible(true));
|
||||||
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
tests/Entity/ProjectMetaTest.php
Normal file
42
tests/Entity/ProjectMetaTest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\ProjectMeta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\ProjectMeta
|
||||||
|
*/
|
||||||
|
class ProjectMetaTest extends AbstractMetaEntityTest
|
||||||
|
{
|
||||||
|
protected function getEntity(): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return new Project();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMetaEntity(): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
return new ProjectMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Expected instanceof Project, received "App\Entity\Customer"
|
||||||
|
*/
|
||||||
|
public function testSetEntityThrowsException()
|
||||||
|
{
|
||||||
|
$sut = new ProjectMeta();
|
||||||
|
$sut->setEntity(new Customer());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ namespace App\Tests\Entity;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Entity\ProjectMeta;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +38,9 @@ class ProjectTest extends TestCase
|
|||||||
$this->assertNull($sut->getColor());
|
$this->assertNull($sut->getColor());
|
||||||
$this->assertEquals(0.0, $sut->getBudget());
|
$this->assertEquals(0.0, $sut->getBudget());
|
||||||
$this->assertEquals(0, $sut->getTimeBudget());
|
$this->assertEquals(0, $sut->getTimeBudget());
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter()
|
||||||
@@ -73,4 +78,31 @@ class ProjectTest extends TestCase
|
|||||||
$this->assertInstanceOf(Project::class, $sut->setTimeBudget(937321));
|
$this->assertInstanceOf(Project::class, $sut->setTimeBudget(937321));
|
||||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMetaFields()
|
||||||
|
{
|
||||||
|
$sut = new Project();
|
||||||
|
$meta = new ProjectMeta();
|
||||||
|
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||||
|
$this->assertInstanceOf(Project::class, $sut->setMetaField($meta));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test', $result->getType());
|
||||||
|
|
||||||
|
$meta2 = new ProjectMeta();
|
||||||
|
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||||
|
$this->assertInstanceOf(Project::class, $sut->setMetaField($meta2));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||||
|
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test2', $result->getType());
|
||||||
|
|
||||||
|
$sut->setMetaField((new ProjectMeta())->setName('blub')->setIsVisible(true));
|
||||||
|
$sut->setMetaField((new ProjectMeta())->setName('blab')->setIsVisible(true));
|
||||||
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
tests/Entity/TimesheetMetaTest.php
Normal file
42
tests/Entity/TimesheetMetaTest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\TimesheetMeta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\TimesheetMeta
|
||||||
|
*/
|
||||||
|
class TimesheetMetaTest extends AbstractMetaEntityTest
|
||||||
|
{
|
||||||
|
protected function getEntity(): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return new Timesheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMetaEntity(): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
return new TimesheetMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Expected instanceof Timesheet, received "App\Entity\Project"
|
||||||
|
*/
|
||||||
|
public function testSetEntityThrowsException()
|
||||||
|
{
|
||||||
|
$sut = new TimesheetMeta();
|
||||||
|
$sut->setEntity(new Project());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,10 @@ use App\Entity\Customer;
|
|||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Tag;
|
use App\Entity\Tag;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\TimesheetMeta;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,11 +41,13 @@ class TimesheetTest extends TestCase
|
|||||||
$this->assertNull($sut->getHourlyRate());
|
$this->assertNull($sut->getHourlyRate());
|
||||||
$this->assertEquals(new ArrayCollection(), $sut->getTags());
|
$this->assertEquals(new ArrayCollection(), $sut->getTags());
|
||||||
$this->assertEquals([], $sut->getTagsAsArray());
|
$this->assertEquals([], $sut->getTagsAsArray());
|
||||||
|
|
||||||
$this->assertInstanceOf(Timesheet::class, $sut->setFixedRate(13.47));
|
$this->assertInstanceOf(Timesheet::class, $sut->setFixedRate(13.47));
|
||||||
$this->assertEquals(13.47, $sut->getFixedRate());
|
$this->assertEquals(13.47, $sut->getFixedRate());
|
||||||
$this->assertInstanceOf(Timesheet::class, $sut->setHourlyRate(99));
|
$this->assertInstanceOf(Timesheet::class, $sut->setHourlyRate(99));
|
||||||
$this->assertEquals(99, $sut->getHourlyRate());
|
$this->assertEquals(99, $sut->getHourlyRate());
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getEntity()
|
protected function getEntity()
|
||||||
@@ -89,4 +93,31 @@ class TimesheetTest extends TestCase
|
|||||||
$sut->removeTag($tag1);
|
$sut->removeTag($tag1);
|
||||||
$this->assertEmpty($sut->getTags());
|
$this->assertEmpty($sut->getTags());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMetaFields()
|
||||||
|
{
|
||||||
|
$sut = new Timesheet();
|
||||||
|
$meta = new TimesheetMeta();
|
||||||
|
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||||
|
$this->assertInstanceOf(Timesheet::class, $sut->setMetaField($meta));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test', $result->getType());
|
||||||
|
|
||||||
|
$meta2 = new TimesheetMeta();
|
||||||
|
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||||
|
$this->assertInstanceOf(Timesheet::class, $sut->setMetaField($meta2));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||||
|
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test2', $result->getType());
|
||||||
|
|
||||||
|
$sut->setMetaField((new TimesheetMeta())->setName('blub')->setIsVisible(true));
|
||||||
|
$sut->setMetaField((new TimesheetMeta())->setName('blab')->setIsVisible(true));
|
||||||
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
tests/Event/ActivityMetaDefinitionEventTest.php
Normal file
27
tests/Event/ActivityMetaDefinitionEventTest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\ActivityMetaDefinitionEvent
|
||||||
|
*/
|
||||||
|
class ActivityMetaDefinitionEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetterAndSetter()
|
||||||
|
{
|
||||||
|
$activity = new Activity();
|
||||||
|
$sut = new ActivityMetaDefinitionEvent($activity);
|
||||||
|
$this->assertSame($activity, $sut->getEntity());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/Event/CustomerMetaDefinitionEventTest.php
Normal file
27
tests/Event/CustomerMetaDefinitionEventTest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\CustomerMetaDefinitionEvent
|
||||||
|
*/
|
||||||
|
class CustomerMetaDefinitionEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetterAndSetter()
|
||||||
|
{
|
||||||
|
$customer = new Customer();
|
||||||
|
$sut = new CustomerMetaDefinitionEvent($customer);
|
||||||
|
$this->assertSame($customer, $sut->getEntity());
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tests/Event/PrepareUserEventTest.php
Normal file
28
tests/Event/PrepareUserEventTest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Event\PrepareUserEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\PrepareUserEvent
|
||||||
|
*/
|
||||||
|
class PrepareUserEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetterAndSetter()
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$sut = new PrepareUserEvent($user);
|
||||||
|
$this->assertEquals('app.prepare_user', PrepareUserEvent::PREPARE);
|
||||||
|
$this->assertSame($user, $sut->getUser());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/Event/ProjectMetaDefinitionEventTest.php
Normal file
27
tests/Event/ProjectMetaDefinitionEventTest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\ProjectMetaDefinitionEvent
|
||||||
|
*/
|
||||||
|
class ProjectMetaDefinitionEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetterAndSetter()
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$sut = new ProjectMetaDefinitionEvent($project);
|
||||||
|
$this->assertSame($project, $sut->getEntity());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/Event/TimesheetMetaDefinitionEventTest.php
Normal file
27
tests/Event/TimesheetMetaDefinitionEventTest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Event\TimesheetMetaDefinitionEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\TimesheetMetaDefinitionEvent
|
||||||
|
*/
|
||||||
|
class TimesheetMetaDefinitionEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetterAndSetter()
|
||||||
|
{
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$sut = new TimesheetMetaDefinitionEvent($timesheet);
|
||||||
|
$this->assertSame($timesheet, $sut->getEntity());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ use App\Entity\Customer;
|
|||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Tag;
|
use App\Entity\Tag;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\TimesheetMeta;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Export\RendererInterface;
|
use App\Export\RendererInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
@@ -139,6 +140,8 @@ abstract class AbstractRendererTest extends KernelTestCase
|
|||||||
->setEnd(new \DateTime('2019-06-16 12:06:40'))
|
->setEnd(new \DateTime('2019-06-16 12:06:40'))
|
||||||
->addTag((new Tag())->setName('foo'))
|
->addTag((new Tag())->setName('foo'))
|
||||||
->addTag((new Tag())->setName('bar'))
|
->addTag((new Tag())->setName('bar'))
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo')->setValue('meta-bar')->setIsVisible(true))
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo2')->setValue('meta-bar2')->setIsVisible(true))
|
||||||
;
|
;
|
||||||
|
|
||||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
|
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$all[] = str_getcsv($row);
|
$all[] = str_getcsv($row);
|
||||||
}
|
}
|
||||||
self::assertEquals(7, count($all));
|
|
||||||
self::assertEquals(13, count($all[0]));
|
|
||||||
self::assertEquals('foo', $all[4][8]);
|
|
||||||
|
|
||||||
$expected = [
|
$expected = [
|
||||||
0 => '2019.06.16 12:00',
|
0 => '2019.06.16 12:00',
|
||||||
@@ -88,12 +85,18 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
6 => '',
|
6 => '',
|
||||||
7 => '',
|
7 => '',
|
||||||
8 => 'foo,bar',
|
8 => 'foo,bar',
|
||||||
9 => '€0.00',
|
9 => 'meta-bar',
|
||||||
10 => '€84.00',
|
10 => 'meta-bar2',
|
||||||
11 => '00:06 h',
|
11 => '€0.00',
|
||||||
12 => '€0.00',
|
12 => '€84.00',
|
||||||
|
13 => '00:06 h',
|
||||||
|
14 => '€0.00',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
self::assertEquals(7, count($all));
|
||||||
|
self::assertEquals(count($expected), count($all[0]));
|
||||||
|
self::assertEquals('foo', $all[4][8]);
|
||||||
|
|
||||||
self::assertEquals($expected, $all[5]);
|
self::assertEquals($expected, $all[5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ class DebugRendererTest extends TestCase
|
|||||||
|
|
||||||
public function getTestModel()
|
public function getTestModel()
|
||||||
{
|
{
|
||||||
yield [$this->getInvoiceModel(), '1,947.99', 5, 5, 1, 2, 2, true];
|
yield [$this->getInvoiceModel(), '1,947.99', 5, 5, 1, 2, 2, true, [['entry.meta.foo-timesheet'], ['entry.meta.foo-timesheet2'], ['entry.meta.foo-timesheet'], ['entry.meta.foo-timesheet3']]];
|
||||||
yield [$this->getInvoiceModelOneEntry(), '293.27', 1, 1, 0, 1, 0, false];
|
yield [$this->getInvoiceModelOneEntry(), '293.27', 1, 1, 0, 1, 0, false, []];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider getTestModel
|
* @dataProvider getTestModel
|
||||||
*/
|
*/
|
||||||
public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3, $hasProject)
|
public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3, $hasProject, $metaFields = [])
|
||||||
{
|
{
|
||||||
$document = new InvoiceDocument(new \SplFileInfo(__DIR__ . '/DebugRenderer.php'));
|
$document = new InvoiceDocument(new \SplFileInfo(__DIR__ . '/DebugRenderer.php'));
|
||||||
$sut = new DebugRenderer();
|
$sut = new DebugRenderer();
|
||||||
@@ -39,14 +39,16 @@ class DebugRendererTest extends TestCase
|
|||||||
$rows = $data['entries'];
|
$rows = $data['entries'];
|
||||||
$this->assertEquals($expectedRows, count($rows));
|
$this->assertEquals($expectedRows, count($rows));
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$this->assertEntryStructure($row);
|
$meta = isset($metaFields[$i]) ? $metaFields[$i++] : [];
|
||||||
|
$this->assertEntryStructure($row, $meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check values or formats?
|
// TODO check values or formats?
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertModelStructure(array $model, $hasProject = true)
|
protected function assertModelStructure(array $model, $hasProject = true, $hasActivity = false)
|
||||||
{
|
{
|
||||||
$keys = [
|
$keys = [
|
||||||
'invoice.due_date',
|
'invoice.due_date',
|
||||||
@@ -77,6 +79,11 @@ class DebugRendererTest extends TestCase
|
|||||||
'customer.number',
|
'customer.number',
|
||||||
'customer.homepage',
|
'customer.homepage',
|
||||||
'customer.comment',
|
'customer.comment',
|
||||||
|
'customer.meta.foo-customer',
|
||||||
|
'activity.id',
|
||||||
|
'activity.name',
|
||||||
|
'activity.comment',
|
||||||
|
'activity.meta.foo-activity',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($hasProject) {
|
if ($hasProject) {
|
||||||
@@ -85,6 +92,15 @@ class DebugRendererTest extends TestCase
|
|||||||
'project.name',
|
'project.name',
|
||||||
'project.comment',
|
'project.comment',
|
||||||
'project.order_number',
|
'project.order_number',
|
||||||
|
'project.meta.foo-project',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasActivity) {
|
||||||
|
$keys = array_merge($keys, [
|
||||||
|
'activity.id',
|
||||||
|
'activity.name',
|
||||||
|
'activity.comment',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +111,7 @@ class DebugRendererTest extends TestCase
|
|||||||
$this->assertEquals($keys, $givenKeys);
|
$this->assertEquals($keys, $givenKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertEntryStructure(array $model)
|
protected function assertEntryStructure(array $model, array $metaFields)
|
||||||
{
|
{
|
||||||
$keys = [
|
$keys = [
|
||||||
'entry.row',
|
'entry.row',
|
||||||
@@ -125,6 +141,8 @@ class DebugRendererTest extends TestCase
|
|||||||
'entry.customer_id',
|
'entry.customer_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$keys = array_merge($keys, $metaFields);
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
$this->assertArrayHasKey($key, $model);
|
$this->assertArrayHasKey($key, $model);
|
||||||
}
|
}
|
||||||
@@ -134,7 +152,7 @@ class DebugRendererTest extends TestCase
|
|||||||
$givenKeys = array_keys($model);
|
$givenKeys = array_keys($model);
|
||||||
sort($givenKeys);
|
sort($givenKeys);
|
||||||
|
|
||||||
$this->assertEquals(count($keys), count($givenKeys));
|
|
||||||
$this->assertEquals($expectedKeys, $givenKeys);
|
$this->assertEquals($expectedKeys, $givenKeys);
|
||||||
|
$this->assertEquals(count($keys), count($givenKeys));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ namespace App\Tests\Invoice\Renderer;
|
|||||||
|
|
||||||
use App\Configuration\LanguageFormattings;
|
use App\Configuration\LanguageFormattings;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\ActivityMeta;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\CustomerMeta;
|
||||||
use App\Entity\InvoiceDocument;
|
use App\Entity\InvoiceDocument;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Entity\ProjectMeta;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\TimesheetMeta;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Invoice\Calculator\DefaultCalculator;
|
use App\Invoice\Calculator\DefaultCalculator;
|
||||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||||
@@ -85,6 +89,8 @@ trait RendererTestTrait
|
|||||||
{
|
{
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setCurrency('EUR');
|
$customer->setCurrency('EUR');
|
||||||
|
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||||
|
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
$template->setTitle('a test invoice template title');
|
$template->setTitle('a test invoice template title');
|
||||||
$template->setVat(19);
|
$template->setVat(19);
|
||||||
@@ -92,10 +98,12 @@ trait RendererTestTrait
|
|||||||
$project = new Project();
|
$project = new Project();
|
||||||
$project->setName('project name');
|
$project->setName('project name');
|
||||||
$project->setCustomer($customer);
|
$project->setCustomer($customer);
|
||||||
|
$project->setMetaField((new ProjectMeta())->setName('foo-project')->setValue('bar-project')->setIsVisible(true));
|
||||||
|
|
||||||
$activity = new Activity();
|
$activity = new Activity();
|
||||||
$activity->setName('activity description');
|
$activity->setName('activity description');
|
||||||
$activity->setProject($project);
|
$activity->setProject($project);
|
||||||
|
$activity->setMetaField((new ActivityMeta())->setName('foo-activity')->setValue('bar-activity')->setIsVisible(true));
|
||||||
|
|
||||||
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
|
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
|
||||||
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||||
@@ -116,7 +124,7 @@ trait RendererTestTrait
|
|||||||
->setProject($project)
|
->setProject($project)
|
||||||
->setBegin(new \DateTime())
|
->setBegin(new \DateTime())
|
||||||
->setEnd(new \DateTime())
|
->setEnd(new \DateTime())
|
||||||
;
|
->setMetaField((new TimesheetMeta())->setName('foo-timesheet')->setValue('bar-timesheet')->setIsVisible(true));
|
||||||
|
|
||||||
$timesheet2 = new Timesheet();
|
$timesheet2 = new Timesheet();
|
||||||
$timesheet2
|
$timesheet2
|
||||||
@@ -127,6 +135,8 @@ trait RendererTestTrait
|
|||||||
->setProject($project)
|
->setProject($project)
|
||||||
->setBegin(new \DateTime())
|
->setBegin(new \DateTime())
|
||||||
->setEnd(new \DateTime())
|
->setEnd(new \DateTime())
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo-timesheet')->setValue('bar-timesheet'))
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo-timesheet2')->setValue('bar-timesheet2')->setIsVisible(true))
|
||||||
;
|
;
|
||||||
|
|
||||||
$timesheet3 = new Timesheet();
|
$timesheet3 = new Timesheet();
|
||||||
@@ -138,6 +148,7 @@ trait RendererTestTrait
|
|||||||
->setProject($project)
|
->setProject($project)
|
||||||
->setBegin(new \DateTime())
|
->setBegin(new \DateTime())
|
||||||
->setEnd(new \DateTime())
|
->setEnd(new \DateTime())
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo-timesheet')->setValue('bar-timesheet1')->setIsVisible(true))
|
||||||
;
|
;
|
||||||
|
|
||||||
$timesheet4 = new Timesheet();
|
$timesheet4 = new Timesheet();
|
||||||
@@ -149,6 +160,7 @@ trait RendererTestTrait
|
|||||||
->setProject($project)
|
->setProject($project)
|
||||||
->setBegin(new \DateTime())
|
->setBegin(new \DateTime())
|
||||||
->setEnd(new \DateTime())
|
->setEnd(new \DateTime())
|
||||||
|
->setMetaField((new TimesheetMeta())->setName('foo-timesheet3')->setValue('bluuuub')->setIsVisible(true))
|
||||||
;
|
;
|
||||||
|
|
||||||
$timesheet5 = new Timesheet();
|
$timesheet5 = new Timesheet();
|
||||||
@@ -193,6 +205,8 @@ trait RendererTestTrait
|
|||||||
{
|
{
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setCurrency('USD');
|
$customer->setCurrency('USD');
|
||||||
|
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||||
|
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
$template->setTitle('a test invoice template title');
|
$template->setTitle('a test invoice template title');
|
||||||
$template->setVat(19);
|
$template->setVat(19);
|
||||||
@@ -200,10 +214,12 @@ trait RendererTestTrait
|
|||||||
$project = new Project();
|
$project = new Project();
|
||||||
$project->setName('project name');
|
$project->setName('project name');
|
||||||
$project->setCustomer($customer);
|
$project->setCustomer($customer);
|
||||||
|
$project->setMetaField((new ProjectMeta())->setName('foo-project')->setValue('bar-project')->setIsVisible(true));
|
||||||
|
|
||||||
$activity = new Activity();
|
$activity = new Activity();
|
||||||
$activity->setName('activity description');
|
$activity->setName('activity description');
|
||||||
$activity->setProject($project);
|
$activity->setProject($project);
|
||||||
|
$activity->setMetaField((new ActivityMeta())->setName('foo-activity')->setValue('bar-activity')->setIsVisible(true));
|
||||||
|
|
||||||
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
|
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
|
||||||
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||||
|
|||||||
37
tests/Mocks/ActivityTestMetaFieldSubscriberMock.php
Normal file
37
tests/Mocks/ActivityTestMetaFieldSubscriberMock.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
|
use App\Entity\ActivityMeta;
|
||||||
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
|
||||||
|
class ActivityTestMetaFieldSubscriberMock implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ActivityMetaDefinitionEvent::class => ['loadMeta', 200],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadMeta(ActivityMetaDefinitionEvent $event)
|
||||||
|
{
|
||||||
|
$definition = (new ActivityMeta())
|
||||||
|
->setName('metatestmock')
|
||||||
|
->setType(TextType::class)
|
||||||
|
->addConstraint(new Length(['max' => 200]))
|
||||||
|
->setIsVisible(true);
|
||||||
|
|
||||||
|
$event->getEntity()->setMetaField($definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/Mocks/CustomerTestMetaFieldSubscriberMock.php
Normal file
37
tests/Mocks/CustomerTestMetaFieldSubscriberMock.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
|
use App\Entity\CustomerMeta;
|
||||||
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
|
||||||
|
class CustomerTestMetaFieldSubscriberMock implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CustomerMetaDefinitionEvent::class => ['loadMeta', 200],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadMeta(CustomerMetaDefinitionEvent $event)
|
||||||
|
{
|
||||||
|
$definition = (new CustomerMeta())
|
||||||
|
->setName('metatestmock')
|
||||||
|
->setType(TextType::class)
|
||||||
|
->addConstraint(new Length(['max' => 200]))
|
||||||
|
->setIsVisible(true);
|
||||||
|
|
||||||
|
$event->getEntity()->setMetaField($definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/Mocks/ProjectTestMetaFieldSubscriberMock.php
Normal file
37
tests/Mocks/ProjectTestMetaFieldSubscriberMock.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
|
use App\Entity\ProjectMeta;
|
||||||
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
|
||||||
|
class ProjectTestMetaFieldSubscriberMock implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ProjectMetaDefinitionEvent::class => ['loadMeta', 200],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadMeta(ProjectMetaDefinitionEvent $event)
|
||||||
|
{
|
||||||
|
$definition = (new ProjectMeta())
|
||||||
|
->setName('metatestmock')
|
||||||
|
->setType(TextType::class)
|
||||||
|
->addConstraint(new Length(['max' => 200]))
|
||||||
|
->setIsVisible(true);
|
||||||
|
|
||||||
|
$event->getEntity()->setMetaField($definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php
Normal file
37
tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
|
use App\Entity\TimesheetMeta;
|
||||||
|
use App\Event\TimesheetMetaDefinitionEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
|
||||||
|
class TimesheetTestMetaFieldSubscriberMock implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TimesheetMetaDefinitionEvent::class => ['loadMeta', 200],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadMeta(TimesheetMetaDefinitionEvent $event)
|
||||||
|
{
|
||||||
|
$definition = (new TimesheetMeta())
|
||||||
|
->setName('metatestmock')
|
||||||
|
->setType(TextType::class)
|
||||||
|
->addConstraint(new Length(['max' => 200]))
|
||||||
|
->setIsVisible(true);
|
||||||
|
|
||||||
|
$event->getEntity()->setMetaField($definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,14 +13,14 @@ use App\Configuration\TimesheetConfiguration;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Tests\Configuration\TestConfigLoader;
|
use App\Tests\Configuration\TestConfigLoader;
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
use App\Timesheet\TrackingMode\DurationFixedStartMode;
|
use App\Timesheet\TrackingMode\DurationFixedBeginMode;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Timesheet\TrackingMode\DurationFixedStartMode
|
* @covers \App\Timesheet\TrackingMode\DurationFixedBeginMode
|
||||||
*/
|
*/
|
||||||
class DurationFixedStartModeTest extends TestCase
|
class DurationFixedBeginModeTest extends TestCase
|
||||||
{
|
{
|
||||||
protected function createSut()
|
protected function createSut()
|
||||||
{
|
{
|
||||||
@@ -28,7 +28,7 @@ class DurationFixedStartModeTest extends TestCase
|
|||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
||||||
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
|
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
|
||||||
|
|
||||||
return new DurationFixedStartMode($dateTime, $configuration);
|
return new DurationFixedBeginMode($dateTime, $configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDefaultValues()
|
public function testDefaultValues()
|
||||||
@@ -40,7 +40,7 @@ class DurationFixedStartModeTest extends TestCase
|
|||||||
self::assertTrue($sut->canEditDuration());
|
self::assertTrue($sut->canEditDuration());
|
||||||
self::assertFalse($sut->canUpdateTimesWithAPI());
|
self::assertFalse($sut->canUpdateTimesWithAPI());
|
||||||
self::assertFalse($sut->canSeeBeginAndEndTimes());
|
self::assertFalse($sut->canSeeBeginAndEndTimes());
|
||||||
self::assertEquals('duration_fixed_start', $sut->getId());
|
self::assertEquals('duration_fixed_begin', $sut->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreate()
|
public function testCreate()
|
||||||
@@ -54,4 +54,14 @@ class DurationFixedStartModeTest extends TestCase
|
|||||||
$sut->create($timesheet, $request);
|
$sut->create($timesheet, $request);
|
||||||
self::assertEquals('13:47', $timesheet->getBegin()->format('H:i'));
|
self::assertEquals('13:47', $timesheet->getBegin()->format('H:i'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateWithoutBeginInjectsBegin()
|
||||||
|
{
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
$sut = $this->createSut();
|
||||||
|
$sut->create($timesheet, $request);
|
||||||
|
self::assertEquals('13:47', $timesheet->getBegin()->format('H:i'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ class TrackingModeServiceTest extends TestCase
|
|||||||
self::assertContains('default', $ids);
|
self::assertContains('default', $ids);
|
||||||
self::assertContains('punch', $ids);
|
self::assertContains('punch', $ids);
|
||||||
self::assertContains('duration_only', $ids);
|
self::assertContains('duration_only', $ids);
|
||||||
self::assertContains('duration_fixed_start', $ids);
|
self::assertContains('duration_fixed_begin', $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetActiveMode()
|
public function testGetActiveMode()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user