API improvements (#1826)

This commit is contained in:
Kevin Papst
2020-07-17 20:30:16 +02:00
committed by GitHub
parent 5864c7e127
commit c843dba29a
77 changed files with 1745 additions and 1189 deletions

View File

@@ -22,6 +22,10 @@ Perform EACH version specific task between your version and the new one, otherwi
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed - **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more - **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working) - **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
- **BC Break**: API changes
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
- new result types were introduced
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9) ## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9)

View File

@@ -14,9 +14,6 @@ jms_serializer:
FOSUB: FOSUB:
namespace_prefix: "FOS\\UserBundle" namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle" path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle"
App:
namespace_prefix: "App"
path: "%kernel.root_dir%/../config/serializer/App"
warmup: warmup:
paths: paths:
included: included:

View File

@@ -2,41 +2,40 @@ nelmio_api_doc:
models: models:
use_jms: true use_jms: true
names: names:
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, 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, Customer_Entity] }
- { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, CustomerRate] } - { alias: Customer, type: App\Entity\Customer, groups: [Default] }
- { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, CustomerRate] } - { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, Customer_Rate] }
- { alias: CustomerMetaField, type: App\Entity\CustomerMeta, groups: [Default, Customer] } - { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, Customer_Rate] }
- { 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\API\ProjectApiEditForm, 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, Project_Entity] }
- { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, ProjectRate] } - { alias: Project, type: App\Entity\Project, groups: [Default] }
- { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, ProjectRate] } - { alias: ProjectExpanded, type: App\Entity\Project, groups: [Default, Expanded] }
- { alias: ProjectMetaField, type: App\Entity\ProjectMeta, groups: [Default, Project] } - { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, Project_Rate] }
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] } - { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, Project_Rate] }
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] } - { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] } - { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] }
- { alias: ActivityRate, type: App\Entity\ActivityRate, groups: [Default, Entity, ActivityRate] } - { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity, Activity_Entity] }
- { alias: ActivityRateForm, type: App\Form\API\ActivityRateApiForm, groups: [Default, Entity, ActivityRate] } - { alias: Activity, type: App\Entity\Activity, groups: [Default] }
- { alias: ActivityMetaField, type: App\Entity\ActivityMeta, groups: [Default, Activity] } - { alias: ActivityExpanded, type: App\Entity\Activity, groups: [Default, Expanded] }
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] } - { alias: ActivityRate, type: App\Entity\ActivityRate, groups: [Default, Entity, Activity_Rate] }
- { alias: TagEditForm, type: App\Form\API\TagApiEditForm, groups: [Default, Entity, Tag] } - { alias: ActivityRateForm, type: App\Form\API\ActivityRateApiForm, groups: [Default, Entity, Activity_Rate] }
- { alias: TagEntity, type: App\Entity\Tag, groups: [Default, Entity, Tag] } - { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
- { alias: TimesheetEditForm, type: App\Form\API\TimesheetApiEditForm, groups: [Default, Entity, Timesheet] } - { alias: TagEditForm, type: App\Form\API\TagApiEditForm, groups: [Default, Entity, Tag] }
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] } - { alias: TagEntity, type: App\Entity\Tag, groups: [Default, Entity, Tag] }
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] } - { alias: TimesheetEditForm, type: App\Form\API\TimesheetApiEditForm, groups: [Default, Entity, Timesheet, Not_Expanded] }
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] } - { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Not_Expanded] }
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] } - { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Not_Expanded] }
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] } - { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Subresource, Expanded] }
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] } - { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
- { alias: User, type: App\Entity\User, groups: [Default, Entity, User] } - { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] } - { alias: User, type: App\Entity\User, groups: [Default] }
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] } - { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] }
- { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team] } - { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] } - { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team, Team_Entity] }
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] } - { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] }
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] } - { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
- { alias: TimesheetConfig, type: App\API\Model\TimesheetConfig, groups: [Default] }
areas: areas:
path_patterns: path_patterns:
- ^/api(?!/doc) - ^/api(?!/doc)
@@ -49,8 +48,9 @@ nelmio_api_doc:
title: Kimai 2 - API Docs title: Kimai 2 - API Docs
description: | description: |
JSON API for the Kimai 2 time-tracking software. Read more about its usage in the [API documentation](https://www.kimai.org/documentation/rest-api.html) and then download a [Swagger file](doc.json) for import e.g. in Postman. JSON API for the Kimai 2 time-tracking software. Read more about its usage in the [API documentation](https://www.kimai.org/documentation/rest-api.html) and then download a [Swagger file](doc.json) for import e.g. in Postman.
Be aware: it is not yet considered stable and BC breaks might happen. Be aware: it is not yet considered stable and BC breaks might happen, especially when using code generation.
version: '0.5' The order of JSON attributes is not guaranteed.
version: '0.6'
securityDefinitions: securityDefinitions:
apiUser: apiUser:
type: apiKey type: apiKey

View File

@@ -1,32 +0,0 @@
App\API\Model\I18n:
exclusion_policy: All
custom_accessor_order: [formDateTime, formDate, dateTime, date, time, duration, is24hours]
properties:
formDateTime:
include: true
type: string
groups: [Default]
formDate:
include: true
type: string
groups: [Default]
dateTime:
include: true
type: string
groups: [Default]
time:
include: true
type: string
groups: [Default]
date:
include: true
type: string
groups: [Default]
duration:
include: true
type: string
groups: [Default]
is24hours:
include: true
type: boolean
groups: [Default]

View File

@@ -1,25 +0,0 @@
App\API\Model\Version:
exclusion_policy: All
custom_accessor_order: [version, candidate, semver, name, copyright]
properties:
version:
include: true
type: string
example: 0.9
groups: [Default]
candidate:
include: true
type: string
groups: [Default]
semver:
include: true
type: string
groups: [Default]
name:
include: true
type: string
groups: [Default]
copyright:
include: true
type: string
groups: [Default]

View File

@@ -1,46 +0,0 @@
App\Entity\Activity:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, project, color, budget, timeBudget, metaFields, parentTitle]
properties:
id:
include: true
groups: [Default]
name:
include: true
groups: [Default]
comment:
include: true
groups: [Entity]
visible:
include: true
groups: [Default]
budget:
include: true
groups: [Entity]
timeBudget:
include: true
groups: [Entity]
project:
include: false
exclude: true
groups: [Default]
color:
include: true
metaFields:
exclude: true
virtual_properties:
parentTitle:
serialized_name: parentTitle
include: true
exp: "object.getProject() === null ? null : object.getProject().getName()"
groups: [Default]
getProject:
serialized_name: project
exp: "object.getProject() === null ? null : object.getProject().getId()"
type: integer
groups: [Default]
getMetaFields:
serialized_name: metaFields
exp: "object.getVisibleMetaFields()"
type: array<App\Entity\ActivityMeta>
groups: [Default]

View File

@@ -1,15 +0,0 @@
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"

View File

@@ -1,21 +0,0 @@
App\Entity\ActivityRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
activity:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -1,68 +0,0 @@
App\Entity\Customer:
exclusion_policy: All
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, color, budget, timeBudget, metaFields, teams]
properties:
id:
include: true
name:
include: true
number:
include: true
groups: [Entity]
comment:
include: true
groups: [Entity]
visible:
include: true
budget:
include: true
groups: [Entity]
timeBudget:
include: true
groups: [Entity]
company:
include: true
groups: [Entity]
contact:
include: true
groups: [Entity]
address:
include: true
groups: [Entity]
country:
include: true
groups: [Entity]
currency:
include: true
groups: [Entity]
phone:
include: true
groups: [Entity]
fax:
include: true
groups: [Entity]
mobile:
include: true
groups: [Entity]
email:
include: true
groups: [Entity]
homepage:
include: true
groups: [Entity]
timezone:
include: true
groups: [Entity]
color:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Customer]
virtual_properties:
getMetaFields:
serialized_name: metaFields
exp: "object.getVisibleMetaFields()"
type: array<App\Entity\CustomerMeta>
groups: [Default]

View File

@@ -1,15 +0,0 @@
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"

View File

@@ -1,21 +0,0 @@
App\Entity\CustomerRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
customer:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -1,56 +0,0 @@
App\Entity\Project:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, color, budget, timeBudget, metaFields, parentTitle, teams]
properties:
id:
include: true
name:
include: true
comment:
include: true
groups: [Entity]
visible:
include: true
budget:
include: true
groups: [Entity]
timeBudget:
include: true
groups: [Entity]
orderNumber:
include: true
groups: [Entity]
orderDate:
include: true
groups: [Entity]
start:
include: true
groups: [Project]
end:
include: true
groups: [Project]
customer:
groups: [Subresource]
color:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Project]
virtual_properties:
parentTitle:
serialized_name: parentTitle
include: true
exp: "object.getCustomer() === null ? null : object.getCustomer().getName()"
groups: [Default]
getCustomer:
serialized_name: customer
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
type: integer
groups: [Entity, Collection]
getMetaFields:
serialized_name: metaFields
exp: "object.getVisibleMetaFields()"
type: array<App\Entity\ProjectMeta>
groups: [Default]

View File

@@ -1,15 +0,0 @@
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"

View File

@@ -1,21 +0,0 @@
App\Entity\ProjectRate:
exclusion_policy: All
custom_accessor_order: [id, rate, internalRate, isFixed, user]
properties:
id:
include: true
groups: [Default]
project:
exclude: true
user:
include: true
groups: [Default]
rate:
include: true
groups: [Default]
internalRate:
include: true
groups: [Default]
isFixed:
include: true
groups: [Default]

View File

@@ -1,12 +0,0 @@
App\Entity\Tag:
exclusion_policy: All
custom_accessor_order: [id, name, color]
properties:
id:
include: true
name:
include: true
color:
include: true
timesheets:
exclude: true

View File

@@ -1,20 +0,0 @@
App\Entity\Team:
exclusion_policy: All
custom_accessor_order: [id, name, teamlead, users, customers, projects]
properties:
id:
include: true
name:
include: true
teamlead:
include: true
groups: [Team_Entity]
users:
include: true
groups: [Team_Entity]
customers:
include: true
groups: [Team_Entity]
projects:
include: true
groups: [Team_Entity]

View File

@@ -1,67 +0,0 @@
App\Entity\Timesheet:
exclusion_policy: All
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate, tags, exported, metaFields]
properties:
id:
include: true
begin:
exclude: true
end:
exclude: true
duration:
include: true
description:
include: true
rate:
include: true
internalRate:
include: true
fixedRate:
include: true
groups: [Entity]
hourlyRate:
include: true
groups: [Entity]
exported:
include: true
groups: [Entity]
activity:
groups: [Subresource]
project:
groups: [Subresource]
user:
exclude: true
metaFields:
exclude: true
virtual_properties:
getBegin:
serialized_name: begin
exp: "object.getBegin() === null ? null : object.getBegin()"
type: DateTime
getEnd:
serialized_name: end
exp: "object.getEnd() === null ? null : object.getEnd()"
type: DateTime
getActivity:
serialized_name: activity
exp: "object.getActivity() === null ? null : object.getActivity().getId()"
type: integer
groups: [Entity, Collection]
getProject:
serialized_name: project
exp: "object.getProject() === null ? null : object.getProject().getId()"
type: integer
groups: [Entity, Collection]
getUser:
serialized_name: user
exp: "object.getUser().getId()"
type: integer
getTags:
serialized_name: tags
exp: "object.getTagsAsArray()"
type: array<string>
getMetaFields:
serialized_name: metaFields
exp: "object.getVisibleMetaFields()"
type: array<App\Entity\TimesheetMeta>
groups: [Default]

View File

@@ -1,15 +0,0 @@
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"

View File

@@ -1,30 +0,0 @@
App\Entity\User:
exclusion_policy: All
custom_accessor_order: [id, alias, title, avatar, language, timezone, teams]
properties:
id:
include: true
groups: [Default]
alias:
include: true
groups: [Default]
title:
include: true
groups: [User_Entity]
avatar:
include: true
groups: [User_Entity]
teams:
include: true
groups: [User_Entity]
virtual_properties:
getLanguage:
serialized_name: language
exp: "object.getPreferenceValue('language') === null ? null : object.getPreferenceValue('language')"
type: string
groups: [User_Entity]
getTimezone:
serialized_name: timezone
exp: "object.getPreferenceValue('timezone') === null ? null : object.getPreferenceValue('timezone')"
type: string
groups: [User_Entity]

View File

@@ -1,16 +1,16 @@
App\Entity\User: App\Entity\User:
properties: properties:
roles: roles:
- App\Validator\Constraints\Role: ~ - App\Validator\Constraints\Role: { groups: [RolesUpdate] }
username: username:
- NotBlank: ~ - NotBlank: { groups: [Registration, UserCreate, Profile] }
- Length: { min: 2, max: 60 } - Length: { min: 2, max: 60, groups: [Registration, UserCreate, Profile] }
email: email:
- NotBlank: ~ - NotBlank: { groups: [Registration, UserCreate, Profile] }
- Email: ~ - Email: { groups: [Registration, UserCreate, Profile] }
plainPassword: plainPassword:
- NotBlank: { groups: [Registration, PasswordUpdate] } - NotBlank: { groups: [Registration, PasswordUpdate, UserCreate] }
- Length: { min: 8, max: 60, groups: [Registration, PasswordUpdate] } - Length: { min: 8, max: 60, groups: [Registration, PasswordUpdate, UserCreate] }
plainApiToken: plainApiToken:
- NotBlank: { groups: [ApiTokenUpdate] } - NotBlank: { groups: [ApiTokenUpdate] }
- Length: { min: 8, max: 60, groups: [ApiTokenUpdate] } - Length: { min: 8, max: 60, groups: [ApiTokenUpdate] }

View File

@@ -41,6 +41,11 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
*/ */
class ActivityController extends BaseApiController class ActivityController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'Activity', 'Activity_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'Activity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Activity'];
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
/** /**
* @var ActivityRepository * @var ActivityRepository
*/ */
@@ -132,7 +137,7 @@ class ActivityController extends BaseApiController
$data = $this->repository->getActivitiesForQuery($query); $data = $this->repository->getActivitiesForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Activity']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -165,7 +170,7 @@ class ActivityController extends BaseApiController
} }
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -212,13 +217,13 @@ class ActivityController extends BaseApiController
$this->repository->saveActivity($activity); $this->repository->saveActivity($activity);
$view = new View($activity, 200); $view = new View($activity, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -275,7 +280,7 @@ class ActivityController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -283,7 +288,7 @@ class ActivityController extends BaseApiController
$this->repository->saveActivity($activity); $this->repository->saveActivity($activity);
$view = new View($activity, Response::HTTP_OK); $view = new View($activity, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -336,7 +341,7 @@ class ActivityController extends BaseApiController
$this->repository->saveActivity($activity); $this->repository->saveActivity($activity);
$view = new View($activity, 200); $view = new View($activity, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -379,7 +384,7 @@ class ActivityController extends BaseApiController
$rates = $this->activityRateRepository->getRatesForActivity($activity); $rates = $this->activityRateRepository->getRatesForActivity($activity);
$view = new View($rates, 200); $view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -490,7 +495,7 @@ class ActivityController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -498,7 +503,7 @@ class ActivityController extends BaseApiController
$this->activityRateRepository->saveRate($rate); $this->activityRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK); $view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ActivityRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace App\API; namespace App\API;
use App\API\Model\I18n; use App\API\Model\I18nConfig;
use App\API\Model\TimesheetConfig; use App\API\Model\TimesheetConfig;
use App\Configuration\LanguageFormattings; use App\Configuration\LanguageFormattings;
use App\Configuration\TimesheetConfiguration; use App\Configuration\TimesheetConfiguration;
@@ -19,6 +19,7 @@ use App\Entity\User;
use FOS\RestBundle\Controller\Annotations as Rest; use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View; use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
@@ -44,11 +45,6 @@ final class ConfigurationController extends BaseApiController
*/ */
private $timesheetConfiguration; private $timesheetConfiguration;
/**
* @param ViewHandlerInterface $viewHandler
* @param LanguageFormattings $formats
* @param TimesheetConfiguration $timesheetConfiguration
*/
public function __construct(ViewHandlerInterface $viewHandler, LanguageFormattings $formats, TimesheetConfiguration $timesheetConfiguration) public function __construct(ViewHandlerInterface $viewHandler, LanguageFormattings $formats, TimesheetConfiguration $timesheetConfiguration)
{ {
$this->viewHandler = $viewHandler; $this->viewHandler = $viewHandler;
@@ -62,7 +58,7 @@ final class ConfigurationController extends BaseApiController
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Returns the locale specific configurations for this user", * description="Returns the locale specific configurations for this user",
* @SWG\Schema(ref="#/definitions/I18nConfig") * @SWG\Schema(ref=@Model(type=I18nConfig::class))
* ) * )
* *
* @Rest\Get(path="/config/i18n") * @Rest\Get(path="/config/i18n")
@@ -76,7 +72,7 @@ final class ConfigurationController extends BaseApiController
$user = $this->getUser(); $user = $this->getUser();
$locale = $user->getLocale(); $locale = $user->getLocale();
$model = new I18n(); $model = new I18nConfig();
$model $model
->setFormDateTime($this->formats->getDateTimeTypeFormat($locale)) ->setFormDateTime($this->formats->getDateTimeTypeFormat($locale))
->setFormDate($this->formats->getDateTypeFormat($locale)) ->setFormDate($this->formats->getDateTypeFormat($locale))
@@ -99,7 +95,7 @@ final class ConfigurationController extends BaseApiController
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Returns the instance specific timesheet configuration", * description="Returns the instance specific timesheet configuration",
* @SWG\Schema(ref="#/definitions/TimesheetConfig") * @SWG\Schema(ref=@Model(type=TimesheetConfig::class))
* ) * )
* *
* @Rest\Get(path="/config/timesheet") * @Rest\Get(path="/config/timesheet")

View File

@@ -42,6 +42,11 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
*/ */
class CustomerController extends BaseApiController class CustomerController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'Customer'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
/** /**
* @var CustomerRepository * @var CustomerRepository
*/ */
@@ -112,7 +117,7 @@ class CustomerController extends BaseApiController
$data = $this->repository->getCustomersForQuery($query); $data = $this->repository->getCustomersForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Customer']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -138,7 +143,7 @@ class CustomerController extends BaseApiController
} }
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -185,13 +190,13 @@ class CustomerController extends BaseApiController
$this->repository->saveCustomer($customer); $this->repository->saveCustomer($customer);
$view = new View($customer, 200); $view = new View($customer, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -248,7 +253,7 @@ class CustomerController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -256,7 +261,7 @@ class CustomerController extends BaseApiController
$this->repository->saveCustomer($customer); $this->repository->saveCustomer($customer);
$view = new View($customer, Response::HTTP_OK); $view = new View($customer, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -309,7 +314,7 @@ class CustomerController extends BaseApiController
$this->repository->saveCustomer($customer); $this->repository->saveCustomer($customer);
$view = new View($customer, 200); $view = new View($customer, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -352,7 +357,7 @@ class CustomerController extends BaseApiController
$rates = $this->customerRateRepository->getRatesForCustomer($customer); $rates = $this->customerRateRepository->getRatesForCustomer($customer);
$view = new View($rates, 200); $view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -463,7 +468,7 @@ class CustomerController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -471,7 +476,7 @@ class CustomerController extends BaseApiController
$this->customerRateRepository->saveRate($rate); $this->customerRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK); $view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'CustomerRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -11,177 +11,127 @@ declare(strict_types=1);
namespace App\API\Model; namespace App\API\Model;
final class I18n use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
*/
final class I18nConfig
{ {
/** /**
* Format used for 'begin' and 'end' * Format used for 'begin' and 'end'
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $formDateTime = ''; private $formDateTime = '';
/** /**
* Format used for toolbar queries * Format used for toolbar queries
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $formDate = ''; private $formDate = '';
/** /**
* Format used to display date-time values (see PHP function date_format) * Format used to display date-time values (see PHP function date_format)
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $dateTime = ''; private $dateTime = '';
/** /**
* Format used to display date values (see PHP function date_format) * Format used to display date values (see PHP function date_format)
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $date = ''; private $date = '';
/** /**
* Format used to display times (see PHP function date_format) * Format used to display times (see PHP function date_format)
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $time = ''; private $time = '';
/** /**
* Format used to display durations (replace: %h with hours, %m with minutes, %s with seconds) * Format used to display durations (replace: %h with hours, %m with minutes, %s with seconds)
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $duration = ''; private $duration = '';
/** /**
* Whether a twenty-four hour format is used (true) or 12-hours AM/PM format (false) * Whether a twenty-four hour format is used (true) or 12-hours AM/PM format (false)
*
* @var bool * @var bool
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="boolean")
*/ */
private $is24hours = true; private $is24hours = true;
/** public function setFormDateTime(string $formDateTime): I18nConfig
* @return string
*/
public function getFormDateTime(): string
{
return $this->formDateTime;
}
/**
* @param string $formDateTime
* @return I18n
*/
public function setFormDateTime(string $formDateTime)
{ {
$this->formDateTime = $formDateTime; $this->formDateTime = $formDateTime;
return $this; return $this;
} }
/** public function setFormDate(string $formDate): I18nConfig
* @return string
*/
public function getFormDate(): string
{
return $this->formDate;
}
/**
* @param string $formDate
* @return I18n
*/
public function setFormDate(string $formDate)
{ {
$this->formDate = $formDate; $this->formDate = $formDate;
return $this; return $this;
} }
/** public function setDateTime(string $dateTime): I18nConfig
* @return string
*/
public function getDateTime(): string
{
return $this->dateTime;
}
/**
* @param string $dateTime
* @return I18n
*/
public function setDateTime(string $dateTime)
{ {
$this->dateTime = $dateTime; $this->dateTime = $dateTime;
return $this; return $this;
} }
/** public function setDate(string $date): I18nConfig
* @return string
*/
public function getDate(): string
{
return $this->date;
}
/**
* @param string $date
* @return I18n
*/
public function setDate(string $date)
{ {
$this->date = $date; $this->date = $date;
return $this; return $this;
} }
/** public function setDuration(string $duration): I18nConfig
* @return string
*/
public function getDuration(): string
{
return $this->duration;
}
/**
* @param string $duration
* @return I18n
*/
public function setDuration(string $duration)
{ {
$this->duration = $duration; $this->duration = $duration;
return $this; return $this;
} }
/** public function setTime(string $time): I18nConfig
* @return string
*/
public function getTime(): string
{
return $this->time;
}
/**
* @param string $time
* @return I18n
*/
public function setTime(string $time)
{ {
$this->time = $time; $this->time = $time;
return $this; return $this;
} }
/** public function setIs24hours(bool $is24hours): I18nConfig
* @return bool
*/
public function isIs24hours(): bool
{
return $this->is24hours;
}
/**
* @param bool $is24hours
* @return I18n
*/
public function setIs24hours(bool $is24hours)
{ {
$this->is24hours = $is24hours; $this->is24hours = $is24hours;

View File

@@ -11,53 +11,74 @@ declare(strict_types=1);
namespace App\API\Model; namespace App\API\Model;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("none")
*/
final class TimesheetConfig final class TimesheetConfig
{ {
/** /**
* See here: https://www.kimai.org/documentation/timesheet.html#tracking-modes * The time-tracking mode, see also: https://www.kimai.org/documentation/timesheet.html#tracking-modes
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $trackingMode = 'default'; private $trackingMode = 'default';
/** /**
* Default begin datetime in PHP format * Default begin datetime in PHP format
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
private $defaultBeginTime = 'now'; private $defaultBeginTime = 'now';
/** /**
* How many running timesheets a user is allowed to have at the same time * How many running timesheets a user is allowed to have at the same time
* *
* @var int * @var int
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="integer")
*/ */
private $activeEntriesHardLimit = 1; private $activeEntriesHardLimit = 1;
/** /**
* How many running timesheets a user is allowed before a warning is shown * How many running timesheets a user is allowed before a warning is shown
* *
* @var int * @var int
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="integer")
*/ */
private $activeEntriesSoftLimit = 1; private $activeEntriesSoftLimit = 1;
/** /**
* Whether entries for future times are allowed * Whether entries for future times are allowed
* *
* @var bool * @var bool
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="boolean")
*/ */
private $isAllowFutureTimes = true; private $isAllowFutureTimes = true;
/** /**
* Whether overlapping entries are allowed * Whether overlapping entries are allowed
* *
* @var bool * @var bool
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="boolean")
*/ */
private $isAllowOverlapping = true; private $isAllowOverlapping = true;
/**
* @return string
*/
public function getTrackingMode(): string
{
return $this->trackingMode;
}
public function setTrackingMode(string $trackingMode): TimesheetConfig public function setTrackingMode(string $trackingMode): TimesheetConfig
{ {
$this->trackingMode = $trackingMode; $this->trackingMode = $trackingMode;
@@ -65,11 +86,6 @@ final class TimesheetConfig
return $this; return $this;
} }
public function getDefaultBeginTime(): string
{
return $this->defaultBeginTime;
}
public function setDefaultBeginTime(string $defaultBeginTime): TimesheetConfig public function setDefaultBeginTime(string $defaultBeginTime): TimesheetConfig
{ {
$this->defaultBeginTime = $defaultBeginTime; $this->defaultBeginTime = $defaultBeginTime;
@@ -77,11 +93,6 @@ final class TimesheetConfig
return $this; return $this;
} }
public function getActiveEntriesHardLimit(): int
{
return $this->activeEntriesHardLimit;
}
public function setActiveEntriesHardLimit(int $activeEntriesHardLimit): TimesheetConfig public function setActiveEntriesHardLimit(int $activeEntriesHardLimit): TimesheetConfig
{ {
$this->activeEntriesHardLimit = $activeEntriesHardLimit; $this->activeEntriesHardLimit = $activeEntriesHardLimit;
@@ -89,11 +100,6 @@ final class TimesheetConfig
return $this; return $this;
} }
public function getActiveEntriesSoftLimit(): int
{
return $this->activeEntriesSoftLimit;
}
public function setActiveEntriesSoftLimit(int $activeEntriesSoftLimit): TimesheetConfig public function setActiveEntriesSoftLimit(int $activeEntriesSoftLimit): TimesheetConfig
{ {
$this->activeEntriesSoftLimit = $activeEntriesSoftLimit; $this->activeEntriesSoftLimit = $activeEntriesSoftLimit;
@@ -101,11 +107,6 @@ final class TimesheetConfig
return $this; return $this;
} }
public function isAllowFutureTimes(): bool
{
return $this->isAllowFutureTimes;
}
public function setIsAllowFutureTimes(bool $isAllowFutureTimes): TimesheetConfig public function setIsAllowFutureTimes(bool $isAllowFutureTimes): TimesheetConfig
{ {
$this->isAllowFutureTimes = $isAllowFutureTimes; $this->isAllowFutureTimes = $isAllowFutureTimes;
@@ -113,11 +114,6 @@ final class TimesheetConfig
return $this; return $this;
} }
public function isAllowOverlapping(): bool
{
return $this->isAllowOverlapping;
}
public function setIsAllowOverlapping(bool $isAllowOverlapping): TimesheetConfig public function setIsAllowOverlapping(bool $isAllowOverlapping): TimesheetConfig
{ {
$this->isAllowOverlapping = $isAllowOverlapping; $this->isAllowOverlapping = $isAllowOverlapping;

View File

@@ -12,37 +12,61 @@ declare(strict_types=1);
namespace App\API\Model; namespace App\API\Model;
use App\Constants; use App\Constants;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
*/
class Version class Version
{ {
/** /**
* Kimai Version, eg. "1.9" * Kimai Version, eg. "1.9"
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
protected $version = Constants::VERSION; protected $version = Constants::VERSION;
/** /**
* Candidate: either "prod" or "dev" * Candidate: either "prod" or "dev"
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
protected $candidate = Constants::STATUS; protected $candidate = Constants::STATUS;
/** /**
* Full version including status, eg: "1.9-prod" * Full version including status, eg: "1.9-prod"
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
protected $semver = Constants::VERSION . '-' . Constants::STATUS; protected $semver = Constants::VERSION . '-' . Constants::STATUS;
/** /**
* The version name * The version name
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
protected $name = Constants::NAME; protected $name = Constants::NAME;
/** /**
* A full copyright notice * A full copyright notice
* *
* @var string * @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="string")
*/ */
protected $copyright = Constants::SOFTWARE . ' - ' . Constants::VERSION . ' ' . Constants::STATUS . ' (' . Constants::NAME . ') by Kevin Papst and contributors.'; protected $copyright = Constants::SOFTWARE . ' - ' . Constants::VERSION . ' ' . Constants::STATUS . ' (' . Constants::NAME . ') by Kevin Papst and contributors.';
} }

View File

@@ -45,6 +45,11 @@ use Symfony\Component\Validator\Constraints;
*/ */
class ProjectController extends BaseApiController class ProjectController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'Project'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
/** /**
* @var ProjectRepository * @var ProjectRepository
*/ */
@@ -159,7 +164,7 @@ class ProjectController extends BaseApiController
$data = $this->repository->getProjectsForQuery($query); $data = $this->repository->getProjectsForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Project']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -185,7 +190,7 @@ class ProjectController extends BaseApiController
} }
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -233,13 +238,13 @@ class ProjectController extends BaseApiController
$this->repository->saveProject($project); $this->repository->saveProject($project);
$view = new View($project, 200); $view = new View($project, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -297,7 +302,7 @@ class ProjectController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -305,7 +310,7 @@ class ProjectController extends BaseApiController
$this->repository->saveProject($project); $this->repository->saveProject($project);
$view = new View($project, Response::HTTP_OK); $view = new View($project, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -358,7 +363,7 @@ class ProjectController extends BaseApiController
$this->repository->saveProject($project); $this->repository->saveProject($project);
$view = new View($project, 200); $view = new View($project, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Project']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -401,7 +406,7 @@ class ProjectController extends BaseApiController
$rates = $this->projectRateRepository->getRatesForProject($project); $rates = $this->projectRateRepository->getRatesForProject($project);
$view = new View($rates, 200); $view = new View($rates, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -512,7 +517,7 @@ class ProjectController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -520,7 +525,7 @@ class ProjectController extends BaseApiController
$this->projectRateRepository->saveRate($rate); $this->projectRateRepository->saveRate($rate);
$view = new View($rate, Response::HTTP_OK); $view = new View($rate, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'ProjectRate']); $view->getContext()->setGroups(self::GROUPS_RATE);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -28,11 +28,8 @@ class StatusController extends BaseApiController
/** /**
* @var ViewHandlerInterface * @var ViewHandlerInterface
*/ */
protected $viewHandler; private $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
*/
public function __construct(ViewHandlerInterface $viewHandler) public function __construct(ViewHandlerInterface $viewHandler)
{ {
$this->viewHandler = $viewHandler; $this->viewHandler = $viewHandler;

View File

@@ -32,22 +32,21 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
* *
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')") * @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/ */
class TagController extends BaseApiController final class TagController extends BaseApiController
{ {
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Tag'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Tag'];
public const GROUPS_FORM = ['Default', 'Entity', 'Tag'];
/** /**
* @var TagRepository * @var TagRepository
*/ */
protected $repository; private $repository;
/** /**
* @var ViewHandlerInterface * @var ViewHandlerInterface
*/ */
protected $viewHandler; private $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
* @param TagRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, TagRepository $repository) public function __construct(ViewHandlerInterface $viewHandler, TagRepository $repository)
{ {
$this->viewHandler = $viewHandler; $this->viewHandler = $viewHandler;
@@ -78,7 +77,7 @@ class TagController extends BaseApiController
$data = $this->repository->findAllTagNames($filter); $data = $this->repository->findAllTagNames($filter);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Tag']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -120,13 +119,13 @@ class TagController extends BaseApiController
$this->repository->saveTag($tag); $this->repository->saveTag($tag);
$view = new View($tag, 200); $view = new View($tag, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Tag']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Tag']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -137,7 +136,7 @@ class TagController extends BaseApiController
* @SWG\Delete( * @SWG\Delete(
* @SWG\Response( * @SWG\Response(
* response=204, * response=204,
* description="Delete one tag" * description="HTTP code 204 for a successful delete"
* ), * ),
* ) * )
* @SWG\Parameter( * @SWG\Parameter(

View File

@@ -39,6 +39,10 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
*/ */
final class TeamController extends BaseApiController final class TeamController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
/** /**
* @var TeamRepository * @var TeamRepository
*/ */
@@ -76,7 +80,7 @@ final class TeamController extends BaseApiController
$data = $this->repository->findAll(); $data = $this->repository->findAll();
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'Team']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -104,7 +108,7 @@ final class TeamController extends BaseApiController
} }
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Team', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -154,7 +158,7 @@ final class TeamController extends BaseApiController
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Returns the new created team", * description="Returns the new created team",
* @SWG\Schema(ref="#/definitions/TeamEntity",), * @SWG\Schema(ref="#/definitions/TeamEntity"),
* ) * )
* ) * )
* @SWG\Parameter( * @SWG\Parameter(
@@ -186,13 +190,13 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, 200); $view = new View($team, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -243,7 +247,7 @@ final class TeamController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -251,7 +255,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -314,7 +318,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -377,7 +381,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -440,7 +444,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -499,7 +503,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -562,7 +566,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -621,7 +625,7 @@ final class TeamController extends BaseApiController
$this->repository->saveTeam($team); $this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK); $view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -51,6 +51,11 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/ */
class TimesheetController extends BaseApiController class TimesheetController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
public const GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
/** /**
* @var TimesheetRepository * @var TimesheetRepository
*/ */
@@ -266,9 +271,9 @@ class TimesheetController extends BaseApiController
$view = new View($data, 200); $view = new View($data, 200);
if ('true' === $paramFetcher->get('full')) { if ('true' === $paramFetcher->get('full')) {
$view->getContext()->setGroups(['Default', 'Subresource', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
} else { } else {
$view->getContext()->setGroups(['Default', 'Collection', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
} }
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
@@ -308,7 +313,7 @@ class TimesheetController extends BaseApiController
} }
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -368,13 +373,13 @@ class TimesheetController extends BaseApiController
} }
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -438,7 +443,7 @@ class TimesheetController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -446,7 +451,7 @@ class TimesheetController extends BaseApiController
$this->service->updateTimesheet($timesheet); $this->service->updateTimesheet($timesheet);
$view = new View($timesheet, Response::HTTP_OK); $view = new View($timesheet, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -498,7 +503,7 @@ class TimesheetController extends BaseApiController
* description="Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)", * description="Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)",
* @SWG\Schema( * @SWG\Schema(
* type="array", * type="array",
* @SWG\Items(ref="#/definitions/TimesheetSubCollection") * @SWG\Items(ref="#/definitions/TimesheetCollectionExpanded")
* ) * )
* ) * )
* *
@@ -535,7 +540,7 @@ class TimesheetController extends BaseApiController
$data = $this->repository->getRecentActivities($user, $begin, $limit); $data = $this->repository->getRecentActivities($user, $begin, $limit);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Subresource', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -548,7 +553,7 @@ class TimesheetController extends BaseApiController
* description="Returns the collection of active timesheet records for the current user", * description="Returns the collection of active timesheet records for the current user",
* @SWG\Schema( * @SWG\Schema(
* type="array", * type="array",
* @SWG\Items(ref="#/definitions/TimesheetSubCollection") * @SWG\Items(ref="#/definitions/TimesheetCollectionExpanded")
* ) * )
* ) * )
* *
@@ -565,7 +570,7 @@ class TimesheetController extends BaseApiController
$data = $this->repository->getActiveEntries($user); $data = $this->repository->getActiveEntries($user);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Subresource', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -604,7 +609,7 @@ class TimesheetController extends BaseApiController
$this->service->stopTimesheet($timesheet); $this->service->stopTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -687,7 +692,7 @@ class TimesheetController extends BaseApiController
$this->service->saveNewTimesheet($copyTimesheet); $this->service->saveNewTimesheet($copyTimesheet);
$view = new View($copyTimesheet, 200); $view = new View($copyTimesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -728,7 +733,7 @@ class TimesheetController extends BaseApiController
$this->service->saveNewTimesheet($copyTimesheet); $this->service->saveNewTimesheet($copyTimesheet);
$view = new View($copyTimesheet, 200); $view = new View($copyTimesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -771,7 +776,7 @@ class TimesheetController extends BaseApiController
$this->service->updateTimesheet($timesheet); $this->service->updateTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -824,7 +829,7 @@ class TimesheetController extends BaseApiController
$this->service->updateTimesheet($timesheet); $this->service->updateTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -39,6 +39,10 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
*/ */
final class UserController extends BaseApiController final class UserController extends BaseApiController
{ {
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
/** /**
* @var UserRepository * @var UserRepository
*/ */
@@ -56,10 +60,6 @@ final class UserController extends BaseApiController
*/ */
private $configuration; private $configuration;
/**
* @param ViewHandlerInterface $viewHandler
* @param UserRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, FormConfiguration $config) public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, FormConfiguration $config)
{ {
$this->viewHandler = $viewHandler; $this->viewHandler = $viewHandler;
@@ -113,7 +113,7 @@ final class UserController extends BaseApiController
$data = $this->repository->getUsersForQuery($query); $data = $this->repository->getUsersForQuery($query);
$view = new View($data, 200); $view = new View($data, 200);
$view->getContext()->setGroups(['Default', 'Collection', 'User']); $view->getContext()->setGroups(self::GROUPS_COLLECTION);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -150,7 +150,7 @@ final class UserController extends BaseApiController
} }
$view = new View($user, 200); $view = new View($user, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -172,7 +172,7 @@ final class UserController extends BaseApiController
public function meAction(): Response public function meAction(): Response
{ {
$view = new View($this->getUser(), 200); $view = new View($this->getUser(), 200);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -223,13 +223,13 @@ final class UserController extends BaseApiController
$this->repository->saveUser($user); $this->repository->saveUser($user);
$view = new View($user, 200); $view = new View($user, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$view = new View($form); $view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -285,7 +285,7 @@ final class UserController extends BaseApiController
if (false === $form->isValid()) { if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK); $view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_FORM);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
@@ -293,7 +293,7 @@ final class UserController extends BaseApiController
$this->repository->saveUser($user); $this->repository->saveUser($user);
$view = new View($user, Response::HTTP_OK); $view = new View($user, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']); $view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }

View File

@@ -363,7 +363,6 @@ class ProfileController extends AbstractController
UserPasswordType::class, UserPasswordType::class,
$user, $user,
[ [
'validation_groups' => ['PasswordUpdate'],
'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUsername()]), 'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUsername()]),
'method' => 'POST' 'method' => 'POST'
] ]

View File

@@ -12,27 +12,49 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
* @ORM\Table(name="kimai2_activities", * @ORM\Table(name="kimai2_activities",
* indexes={ * indexes={
* @ORM\Index(columns={"visible","project_id"}), * @ORM\Index(columns={"visible","project_id"}),
* @ORM\Index(columns={"visible","project_id","name"}), * @ORM\Index(columns={"visible","project_id","name"}),
* @ORM\Index(columns={"visible","name"}) * @ORM\Index(columns={"visible","name"})
* } * }
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository") * @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
* *
* columns={"visible","name"} => IDX_8811FE1C7AB0E8595E237E06 => activity administration without filter * @Serializer\ExclusionPolicy("all")
* columns={"visible","project_id"} => IDX_8811FE1C7AB0E859166D1F9C => activity administration with customer or project filter * @Serializer\VirtualProperty(
* columns={"visible","project_id","name"} => IDX_8811FE1C7AB0E859166D1F9C5E237E06 => activity drop-down for global activities in toolbar or globalsOnly filter in activity administration * "ProjectName",
* exp="object.getProject() === null ? null : object.getProject().getName()",
* options={
* @Serializer\SerializedName("parentTitle"),
* @Serializer\Type(name="string"),
* @Serializer\Groups({"Activity"})
* }
* )
* @Serializer\VirtualProperty(
* "ProjectAsId",
* exp="object.getProject() === null ? null : object.getProject().getId()",
* options={
* @Serializer\SerializedName("project"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Default"})
* }
* )
*/ */
class Activity implements EntityWithMetaFields class Activity implements EntityWithMetaFields
{ {
/** /**
* Internal ID
*
* @var int|null * @var int|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
@@ -46,34 +68,82 @@ class Activity implements EntityWithMetaFields
*/ */
private $project; private $project;
/** /**
* Name of this activity
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=150, nullable=false) * @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(min=2, max=150, allowEmptyString=false) * @Assert\Length(min=2, max=150, allowEmptyString=false)
*/ */
private $name; private $name;
/** /**
* Description of this activity
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Activity_Entity"})
*
* @ORM\Column(name="comment", type="text", nullable=true) * @ORM\Column(name="comment", type="text", nullable=true)
*/ */
private $comment; private $comment;
/** /**
* Whether this activity is visible and can be used for timesheets
*
* @var bool * @var bool
* *
* @ORM\Column(name="visible", type="boolean", nullable=false) * @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="visible", type="boolean", nullable=false, options={"default": true})
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $visible = true; private $visible = true;
// keep the trait include exactly here, for placing the column at the correct position // keep the traits here, for placing the column at the "correct" position
use ColorTrait; use ColorTrait;
use BudgetTrait;
/** /**
* The total monetary budget, will be zero if unconfigured.
*
* @var float
*
* @Serializer\Expose()
* @Serializer\Groups({"Activity_Entity"})
*
* @ORM\Column(name="budget", type="float", nullable=false)
* @Assert\NotNull()
*/
private $budget = 0.00;
/**
* The time budget in seconds, will be be zero if unconfigured.
*
* @var int
*
* @Serializer\Expose()
* @Serializer\Groups({"Activity_Entity"})
*
* @ORM\Column(name="time_budget", type="integer", nullable=false)
* @Assert\NotNull()
*/
private $timeBudget = 0;
/**
* Meta fields
*
* All visible meta (custom) fields registered with this activity
*
* @var ActivityMeta[]|Collection * @var ActivityMeta[]|Collection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Activity"})
* @Serializer\Type(name="array<App\Entity\ActivityMeta>")
* @Serializer\SerializedName("metaFields")
* @Serializer\Accessor(getter="getVisibleMetaFields")
*
* @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"}) * @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"})
*/ */
private $meta; private $meta;
@@ -141,6 +211,30 @@ class Activity implements EntityWithMetaFields
return $this->visible; return $this->visible;
} }
public function setBudget(float $budget): Activity
{
$this->budget = $budget;
return $this;
}
public function getBudget(): float
{
return $this->budget;
}
public function setTimeBudget(int $seconds): Activity
{
$this->timeBudget = $seconds;
return $this;
}
public function getTimeBudget(): int
{
return $this->timeBudget;
}
/** /**
* @return Collection|MetaTableTypeInterface[] * @return Collection|MetaTableTypeInterface[]
*/ */

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -21,6 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\UniqueConstraint(columns={"activity_id", "name"}) * @ORM\UniqueConstraint(columns={"activity_id", "name"})
* } * }
* ) * )
* @Serializer\ExclusionPolicy("all")
*/ */
class ActivityMeta implements MetaTableTypeInterface class ActivityMeta implements MetaTableTypeInterface
{ {

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,6 +22,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository") * @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository")
* @UniqueEntity({"user", "activity"}, ignoreNull=false) * @UniqueEntity({"user", "activity"}, ignoreNull=false)
*
* @Serializer\ExclusionPolicy("all")
*/ */
class ActivityRate implements RateInterface class ActivityRate implements RateInterface
{ {
@@ -29,6 +32,8 @@ class ActivityRate implements RateInterface
/** /**
* @var Activity * @var Activity
* *
* @Serializer\Exclude()
*
* @ORM\ManyToOne(targetEntity="App\Entity\Activity") * @ORM\ManyToOne(targetEntity="App\Entity\Activity")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()

View File

@@ -1,69 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
trait BudgetTrait
{
/**
* @var float
*
* @ORM\Column(name="budget", type="float", nullable=false)
* @Assert\NotNull()
*/
private $budget = 0.00;
/**
* Time budget in seconds.
*
* @var int
*
* @ORM\Column(name="time_budget", type="integer", nullable=false)
* @Assert\NotNull()
*/
private $timeBudget = 0;
/**
* @param float $budget
* @return self
*/
public function setBudget(?float $budget)
{
$this->budget = $budget;
return $this;
}
/**
* @return float
*/
public function getBudget()
{
return $this->budget;
}
/**
* @param int $seconds
* @return self
*/
public function setTimeBudget(?int $seconds)
{
$this->timeBudget = $seconds;
return $this;
}
public function getTimeBudget(): int
{
return $this->timeBudget;
}
}

View File

@@ -10,13 +10,21 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
trait ColorTrait trait ColorTrait
{ {
/** /**
* The assigned color in HTML hex format, eg. #dd1d00
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="color", type="string", length=7, nullable=true) * @ORM\Column(name="color", type="string", length=7, nullable=true)
* @Assert\Length(min=4, max=7)
*/ */
private $color = null; private $color = null;

View File

@@ -12,6 +12,8 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -22,14 +24,17 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository") * @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
* *
* columns={"visible"} => IDX_5A9760447AB0E859 => used in customer dropdown * @Serializer\ExclusionPolicy("all")
*/ */
class Customer implements EntityWithMetaFields class Customer implements EntityWithMetaFields
{ {
public const DEFAULT_CURRENCY = 'EUR'; public const DEFAULT_CURRENCY = 'EUR';
/** /**
* @var int * @var int|null
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* *
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
@@ -39,6 +44,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=150, nullable=false) * @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(min=2, max=150, allowEmptyString=false) * @Assert\Length(min=2, max=150, allowEmptyString=false)
@@ -47,6 +55,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="number", type="string", length=50, nullable=true) * @ORM\Column(name="number", type="string", length=50, nullable=true)
* @Assert\Length(max=50) * @Assert\Length(max=50)
*/ */
@@ -54,12 +65,18 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="comment", type="text", nullable=true) * @ORM\Column(name="comment", type="text", nullable=true)
*/ */
private $comment; private $comment;
/** /**
* @var bool * @var bool
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="visible", type="boolean", nullable=false) * @ORM\Column(name="visible", type="boolean", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
@@ -67,6 +84,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="company", type="string", length=255, nullable=true) * @ORM\Column(name="company", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
@@ -74,6 +94,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true) * @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
* @Assert\Length(max=50) * @Assert\Length(max=50)
*/ */
@@ -81,6 +104,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="contact", type="string", length=255, nullable=true) * @ORM\Column(name="contact", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
@@ -88,12 +114,18 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="address", type="text", nullable=true) * @ORM\Column(name="address", type="text", nullable=true)
*/ */
private $address; private $address;
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="country", type="string", length=2, nullable=false) * @ORM\Column(name="country", type="string", length=2, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(max=2) * @Assert\Length(max=2)
@@ -102,6 +134,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer"})
*
* @ORM\Column(name="currency", type="string", length=3, nullable=false) * @ORM\Column(name="currency", type="string", length=3, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(max=3) * @Assert\Length(max=3)
@@ -110,6 +145,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="phone", type="string", length=255, nullable=true) * @ORM\Column(name="phone", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
@@ -117,6 +155,9 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="fax", type="string", length=255, nullable=true) * @ORM\Column(name="fax", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
@@ -124,15 +165,23 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="mobile", type="string", length=255, nullable=true) * @ORM\Column(name="mobile", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
private $mobile; private $mobile;
/** /**
* @var string * Customers contact email
* *
* Limited via RFC to 254 chars * Limited via RFC to 254 chars
* *
* @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="email", type="string", length=255, nullable=true) * @ORM\Column(name="email", type="string", length=255, nullable=true)
* @Assert\Length(max=254) * @Assert\Length(max=254)
*/ */
@@ -140,15 +189,23 @@ class Customer implements EntityWithMetaFields
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="homepage", type="string", length=255, nullable=true) * @ORM\Column(name="homepage", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
private $homepage; private $homepage;
/** /**
* @var string * Timezone of begin and end
* *
* Length was determined by a MySQL column via "use mysql;describe time_zone_name;" * Length was determined by a MySQL column via "use mysql;describe time_zone_name;"
* *
* @var string
*
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="timezone", type="string", length=64, nullable=false) * @ORM\Column(name="timezone", type="string", length=64, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(max=64) * @Assert\Length(max=64)
@@ -157,17 +214,58 @@ class Customer implements EntityWithMetaFields
// keep the trait include exactly here, for placing the column at the correct position // keep the trait include exactly here, for placing the column at the correct position
use ColorTrait; use ColorTrait;
use BudgetTrait;
/** /**
* The total monetary budget, will be zero if unconfigured.
*
* @var float
*
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="budget", type="float", nullable=false)
* @Assert\NotNull()
*/
private $budget = 0.00;
/**
* The time budget in seconds, will be be zero if unconfigured.
*
* @var int
*
* @Serializer\Expose()
* @Serializer\Groups({"Customer_Entity"})
*
* @ORM\Column(name="time_budget", type="integer", nullable=false)
* @Assert\NotNull()
*/
private $timeBudget = 0;
/**
* Meta fields
*
* All visible meta (custom) fields registered with this customer
*
* @var CustomerMeta[]|Collection * @var CustomerMeta[]|Collection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer"})
* @Serializer\Type(name="array<App\Entity\CustomerMeta>")
* @Serializer\SerializedName("metaFields")
* @Serializer\Accessor(getter="getVisibleMetaFields")
*
* @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"}) * @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"})
*/ */
private $meta; private $meta;
/** /**
* Teams
*
* If no team is assigned, everyone can access the customer
*
* @var Team[]|ArrayCollection * @var Team[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Customer"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
*
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="customers") * @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="customers")
* @ORM\JoinTable( * @ORM\JoinTable(
* name="kimai2_customers_teams", * name="kimai2_customers_teams",
@@ -384,6 +482,30 @@ class Customer implements EntityWithMetaFields
return $this->timezone; return $this->timezone;
} }
public function setBudget(float $budget): Customer
{
$this->budget = $budget;
return $this;
}
public function getBudget(): float
{
return $this->budget;
}
public function setTimeBudget(int $seconds): Customer
{
$this->timeBudget = $seconds;
return $this;
}
public function getTimeBudget(): int
{
return $this->timeBudget;
}
/** /**
* @return Collection|MetaTableTypeInterface[] * @return Collection|MetaTableTypeInterface[]
*/ */

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -21,6 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\UniqueConstraint(columns={"customer_id", "name"}) * @ORM\UniqueConstraint(columns={"customer_id", "name"})
* } * }
* ) * )
* @Serializer\ExclusionPolicy("all")
*/ */
class CustomerMeta implements MetaTableTypeInterface class CustomerMeta implements MetaTableTypeInterface
{ {

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,6 +22,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository") * @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository")
* @UniqueEntity({"user", "customer"}, ignoreNull=false) * @UniqueEntity({"user", "customer"}, ignoreNull=false)
*
* @Serializer\ExclusionPolicy("all")
*/ */
class CustomerRate implements RateInterface class CustomerRate implements RateInterface
{ {
@@ -29,6 +32,8 @@ class CustomerRate implements RateInterface
/** /**
* @var Customer * @var Customer
* *
* @Serializer\Exclude()
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer") * @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
@@ -21,6 +22,8 @@ trait MetaTableTypeTrait
/** /**
* @var int|null * @var int|null
* *
* @Serializer\Exclude()
*
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
@@ -28,8 +31,13 @@ trait MetaTableTypeTrait
private $id; private $id;
/** /**
* Name of the meta (custom) field
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=50, nullable=false) * @ORM\Column(name="name", type="string", length=50, nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
* @Assert\Length(min=2, max=50, allowEmptyString=false) * @Assert\Length(min=2, max=50, allowEmptyString=false)
@@ -37,8 +45,13 @@ trait MetaTableTypeTrait
private $name; private $name;
/** /**
* Value of the meta (custom) field
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="value", type="string", length=255, nullable=true) * @ORM\Column(name="value", type="string", length=255, nullable=true)
*/ */
private $value; private $value;

View File

@@ -9,9 +9,12 @@
namespace App\Entity; namespace App\Entity;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -22,40 +25,78 @@ use Symfony\Component\Validator\Constraints as Assert;
* } * }
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository") * @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
* @App\Validator\Constraints\Project * @Constraints\Project
* *
* columns={"customer_id","visible","name"} => IDX_407F12069395C3F37AB0E8595E237E06 => project administration without filter * @Serializer\ExclusionPolicy("all")
* columns={"customer_id","visible","id"} => IDX_407F12069395C3F37AB0E859BF396750 => used in joins between project and customer, eg. dropdowns and activity administration page * @Serializer\VirtualProperty(
* "CustomerName",
* exp="object.getCustomer() === null ? null : object.getCustomer().getName()",
* options={
* @Serializer\SerializedName("parentTitle"),
* @Serializer\Type(name="string"),
* @Serializer\Groups({"Project"})
* }
* )
* @Serializer\VirtualProperty(
* "CustomerAsId",
* exp="object.getCustomer() === null ? null : object.getCustomer().getId()",
* options={
* @Serializer\SerializedName("customer"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Project", "Team", "Not_Expanded"})
* }
* )
*/ */
class Project implements EntityWithMetaFields class Project implements EntityWithMetaFields
{ {
/** /**
* Internal ID
*
* @var int|null * @var int|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
*/ */
private $id; private $id;
/** /**
* Customer for this project
*
* @var Customer * @var Customer
* *
* @Serializer\Expose()
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Customer"))
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer") * @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $customer; private $customer;
/** /**
* Project name
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=150, nullable=false) * @ORM\Column(name="name", type="string", length=150, nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
* @Assert\Length(min=2, max=150, allowEmptyString=false) * @Assert\Length(min=2, max=150, allowEmptyString=false)
*/ */
private $name; private $name;
/** /**
* Project order number
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project_Entity"})
*
* @ORM\Column(name="order_number", type="text", length=20, nullable=true) * @ORM\Column(name="order_number", type="text", length=20, nullable=true)
* @Assert\Length(max=20) * @Assert\Length(max=20)
*/ */
@@ -63,40 +104,60 @@ class Project implements EntityWithMetaFields
/** /**
* @var \DateTime * @var \DateTime
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project_Entity"})
* @Serializer\Type(name="DateTime")
*
* @ORM\Column(name="order_date", type="datetime", nullable=true) * @ORM\Column(name="order_date", type="datetime", nullable=true)
*/ */
private $orderDate; private $orderDate;
/** /**
* @var \DateTime * @var \DateTime
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project"})
* @Serializer\Type(name="DateTime")
*
* @ORM\Column(name="start", type="datetime", nullable=true) * @ORM\Column(name="start", type="datetime", nullable=true)
*/ */
private $start; private $start;
/** /**
* @var \DateTime * @var \DateTime
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project"})
* @Serializer\Type(name="DateTime")
*
* @ORM\Column(name="end", type="datetime", nullable=true) * @ORM\Column(name="end", type="datetime", nullable=true)
*/ */
private $end; private $end;
/** /**
* @var string * @var string
* @internal used for storing the timezone for "order", "start" and "end" date
* *
* @ORM\Column(name="timezone", type="string", length=64, nullable=true) * @ORM\Column(name="timezone", type="string", length=64, nullable=true)
*/ */
private $timezone; private $timezone;
/** /**
* @var bool * @var bool
* @internal used for having the localization state of the dates (see $timezone)
*/ */
private $localized = false; private $localized = false;
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project_Entity"})
*
* @ORM\Column(name="comment", type="text", nullable=true) * @ORM\Column(name="comment", type="text", nullable=true)
*/ */
private $comment; private $comment;
/** /**
* @var bool * @var bool
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="visible", type="boolean", nullable=false) * @ORM\Column(name="visible", type="boolean", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
@@ -104,18 +165,58 @@ class Project implements EntityWithMetaFields
// keep the trait include exactly here, for placing the column at the correct position // keep the trait include exactly here, for placing the column at the correct position
use ColorTrait; use ColorTrait;
use BudgetTrait;
/** /**
* The total monetary budget, will be zero if unconfigured.
*
* @var float
*
* @Serializer\Expose()
* @Serializer\Groups({"Project_Entity"})
*
* @ORM\Column(name="budget", type="float", nullable=false)
* @Assert\NotNull()
*/
private $budget = 0.00;
/**
* The time budget in seconds, will be be zero if unconfigured.
*
* @var int
*
* @Serializer\Expose()
* @Serializer\Groups({"Project_Entity"})
*
* @ORM\Column(name="time_budget", type="integer", nullable=false)
* @Assert\NotNull()
*/
private $timeBudget = 0;
/**
* Meta fields
*
* All visible meta (custom) fields registered with this project
*
* @var ProjectMeta[]|Collection * @var ProjectMeta[]|Collection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project"})
* @Serializer\Type(name="array<App\Entity\ProjectMeta>")
* @Serializer\SerializedName("metaFields")
* @Serializer\Accessor(getter="getVisibleMetaFields")
*
* @ORM\OneToMany(targetEntity="App\Entity\ProjectMeta", mappedBy="project", cascade={"persist"}) * @ORM\OneToMany(targetEntity="App\Entity\ProjectMeta", mappedBy="project", cascade={"persist"})
*/ */
private $meta; private $meta;
/** /**
* Teams
*
* If no team is assigned, everyone can access the project (also depends on the teams of the customer)
*
* @var Team[]|ArrayCollection * @var Team[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Project"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
*
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="projects") * @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="projects")
* @ORM\JoinTable( * @ORM\JoinTable(
* name="kimai2_projects_teams", * name="kimai2_projects_teams",
@@ -164,11 +265,7 @@ class Project implements EntityWithMetaFields
return $this->name; return $this->name;
} }
/** public function setComment(?string $comment): Project
* @param string $comment
* @return Project
*/
public function setComment($comment): Project
{ {
$this->comment = $comment; $this->comment = $comment;
@@ -289,6 +386,30 @@ class Project implements EntityWithMetaFields
return $this; return $this;
} }
public function setBudget(float $budget): Project
{
$this->budget = $budget;
return $this;
}
public function getBudget(): float
{
return $this->budget;
}
public function setTimeBudget(int $seconds): Project
{
$this->timeBudget = $seconds;
return $this;
}
public function getTimeBudget(): int
{
return $this->timeBudget;
}
/** /**
* @return Collection|MetaTableTypeInterface[] * @return Collection|MetaTableTypeInterface[]
*/ */

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -21,6 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\UniqueConstraint(columns={"project_id", "name"}) * @ORM\UniqueConstraint(columns={"project_id", "name"})
* } * }
* ) * )
* @Serializer\ExclusionPolicy("all")
*/ */
class ProjectMeta implements MetaTableTypeInterface class ProjectMeta implements MetaTableTypeInterface
{ {

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,6 +22,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository") * @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository")
* @UniqueEntity({"user", "project"}, ignoreNull=false) * @UniqueEntity({"user", "project"}, ignoreNull=false)
*
* @Serializer\ExclusionPolicy("all")
*/ */
class ProjectRate implements RateInterface class ProjectRate implements RateInterface
{ {
@@ -29,6 +32,8 @@ class ProjectRate implements RateInterface
/** /**
* @var Project * @var Project
* *
* @Serializer\Exclude()
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project") * @ORM\ManyToOne(targetEntity="App\Entity\Project")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull * @Assert\NotNull

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -18,6 +19,9 @@ trait Rate
/** /**
* @var int|null * @var int|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
@@ -26,14 +30,20 @@ trait Rate
/** /**
* @var User * @var User
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @SWG\Property(ref="#/definitions/User")
*
* @ORM\ManyToOne(targetEntity="App\Entity\User") * @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true) * @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
* @SWG\Property(ref="#/definitions/User")
*/ */
private $user; private $user;
/** /**
* @var float * @var float
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="rate", type="float", nullable=false) * @ORM\Column(name="rate", type="float", nullable=false)
* @Assert\GreaterThanOrEqual(0) * @Assert\GreaterThanOrEqual(0)
*/ */
@@ -41,12 +51,18 @@ trait Rate
/** /**
* @var float|null * @var float|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="internal_rate", type="float", nullable=true) * @ORM\Column(name="internal_rate", type="float", nullable=true)
*/ */
private $internalRate; private $internalRate;
/** /**
* @var bool * @var bool
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="fixed", type="boolean", nullable=false) * @ORM\Column(name="fixed", type="boolean", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -22,21 +23,32 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\TagRepository") * @ORM\Entity(repositoryClass="App\Repository\TagRepository")
* @UniqueEntity("name") * @UniqueEntity("name")
*
* @Serializer\ExclusionPolicy("all")
*/ */
class Tag class Tag
{ {
/** /**
* The internal ID
*
* @var int * @var int
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
*/ */
private $id; private $id;
/** /**
* The tag name
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=100, nullable=false) * @ORM\Column(name="name", type="string", length=100, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(min=2, max=100, allowEmptyString=false) * @Assert\Length(min=2, max=100, allowEmptyString=false)
@@ -49,9 +61,11 @@ class Tag
/** /**
* @var Timesheet[]|ArrayCollection * @var Timesheet[]|ArrayCollection
* *
* @Serializer\Exclude()
*
* @ORM\ManyToMany(targetEntity="Timesheet", mappedBy="tags", fetch="EXTRA_LAZY") * @ORM\ManyToMany(targetEntity="Timesheet", mappedBy="tags", fetch="EXTRA_LAZY")
*/ */
protected $timesheets; private $timesheets;
public function __construct() public function __construct()
{ {

View File

@@ -12,6 +12,8 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -23,48 +25,92 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\TeamRepository") * @ORM\Entity(repositoryClass="App\Repository\TeamRepository")
* @UniqueEntity("name") * @UniqueEntity("name")
*
* @Serializer\ExclusionPolicy("all")
*/ */
class Team class Team
{ {
/** /**
* The internal ID
*
* @var int * @var int
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
*/ */
private $id; private $id;
/** /**
* Team name
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="name", type="string", length=100, nullable=false) * @ORM\Column(name="name", type="string", length=100, nullable=false)
* @Assert\NotBlank() * @Assert\NotBlank()
* @Assert\Length(min=2, max=100, allowEmptyString=false) * @Assert\Length(min=2, max=100, allowEmptyString=false)
*/ */
private $name; private $name;
/** /**
* Teamlead
*
* The teamlead for this team
*
* @var User * @var User
* *
* @Serializer\Expose()
* @Serializer\Groups({"Team_Entity"})
* @SWG\Property(ref="#/definitions/User")
*
* @ORM\ManyToOne(targetEntity="App\Entity\User") * @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $teamlead; private $teamlead;
/** /**
* Team member
*
* All team member, including the teamlead
*
* @var User[]|ArrayCollection * @var User[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Team_Entity"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/User"))
*
* @ORM\ManyToMany(targetEntity="User", mappedBy="teams", fetch="EXTRA_LAZY") * @ORM\ManyToMany(targetEntity="User", mappedBy="teams", fetch="EXTRA_LAZY")
*/ */
private $users; private $users;
/** /**
* Customers
*
* All customers assigned to the team
*
* @var Customer[]|ArrayCollection * @var Customer[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Team_Entity"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Customer"))
*
* @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY") * @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY")
*/ */
private $customers; private $customers;
/** /**
* Projects
*
* All projects assigned to the team
*
* @var Project[]|ArrayCollection * @var Project[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Team_Entity", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Project"))
*
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY") * @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
*/ */
private $projects; private $projects;

View File

@@ -10,12 +10,15 @@
namespace App\Entity; namespace App\Entity;
use App\Export\ExportItemInterface; use App\Export\ExportItemInterface;
use App\Validator\Constraints as Constraints;
use DateTime; use DateTime;
use DateTimeZone; use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo; use Gedmo\Mapping\Annotation as Gedmo;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -31,7 +34,45 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository") * @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
* @ORM\HasLifecycleCallbacks() * @ORM\HasLifecycleCallbacks()
* @App\Validator\Constraints\Timesheet * @Constraints\Timesheet
*
* @Serializer\ExclusionPolicy("all")
* @Serializer\VirtualProperty(
* "ActivityAsId",
* exp="object.getActivity() === null ? null : object.getActivity().getId()",
* options={
* @Serializer\SerializedName("activity"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Not_Expanded"})
* }
* )
* @Serializer\VirtualProperty(
* "ProjectAsId",
* exp="object.getProject() === null ? null : object.getProject().getId()",
* options={
* @Serializer\SerializedName("project"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Not_Expanded"})
* }
* )
* @Serializer\VirtualProperty(
* "UserAsId",
* exp="object.getUser().getId()",
* options={
* @Serializer\SerializedName("user"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Default"})
* }
* )
* @Serializer\VirtualProperty(
* "TagsAsArray",
* exp="object.getTagsAsArray()",
* options={
* @Serializer\SerializedName("tags"),
* @Serializer\Type(name="array<string>"),
* @Serializer\Groups({"Default"})
* }
* )
*/ */
class Timesheet implements EntityWithMetaFields, ExportItemInterface class Timesheet implements EntityWithMetaFields, ExportItemInterface
{ {
@@ -59,47 +100,57 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
/** /**
* @var int|null * @var int|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\GeneratedValue(strategy="IDENTITY")
*/ */
private $id; private $id;
/** /**
* @var DateTime * @var DateTime
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="DateTime")
*
* @ORM\Column(name="start_time", type="datetime", nullable=false) * @ORM\Column(name="start_time", type="datetime", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $begin; private $begin;
/** /**
* @var DateTime * @var DateTime
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* @Serializer\Type(name="DateTime")
*
* @ORM\Column(name="end_time", type="datetime", nullable=true) * @ORM\Column(name="end_time", type="datetime", nullable=true)
*/ */
private $end; private $end;
/** /**
* @var string * @var string
* @internal for storing the timezone of "begin" and "end" date
* *
* @ORM\Column(name="timezone", type="string", length=64, nullable=false) * @ORM\Column(name="timezone", type="string", length=64, nullable=false)
*/ */
private $timezone; private $timezone;
/** /**
* @var bool * @var bool
* @internal for storing the localized state of dates (see $timezone)
*/ */
private $localized = false; private $localized = false;
/** /**
* @var int * @var int
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="duration", type="integer", nullable=true) * @ORM\Column(name="duration", type="integer", nullable=true)
* @Assert\GreaterThanOrEqual(0) * @Assert\GreaterThanOrEqual(0)
*/ */
private $duration = 0; private $duration = 0;
/** /**
* @var User * @var User
* *
@@ -108,71 +159,92 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $user; private $user;
/** /**
* Activity
*
* @var Activity * @var Activity
* *
* @Serializer\Expose()
* @Serializer\Groups({"Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/ActivityExpanded"))
*
* @ORM\ManyToOne(targetEntity="App\Entity\Activity") * @ORM\ManyToOne(targetEntity="App\Entity\Activity")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $activity; private $activity;
/** /**
* Project
*
* @var Project * @var Project
* *
* @Serializer\Expose()
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/ProjectExpanded"))
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project") * @ORM\ManyToOne(targetEntity="App\Entity\Project")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false) * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $project; private $project;
/** /**
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="description", type="text", nullable=true) * @ORM\Column(name="description", type="text", nullable=true)
*/ */
private $description; private $description;
/** /**
* @var float * @var float
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="rate", type="float", nullable=false) * @ORM\Column(name="rate", type="float", nullable=false)
* @Assert\GreaterThanOrEqual(0) * @Assert\GreaterThanOrEqual(0)
*/ */
private $rate = 0.00; private $rate = 0.00;
/** /**
* @var float|null * @var float|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="internal_rate", type="float", nullable=true) * @ORM\Column(name="internal_rate", type="float", nullable=true)
*/ */
private $internalRate; private $internalRate;
/** /**
* @var float|null * @var float|null
* *
* @Serializer\Expose()
* @Serializer\Groups({"Entity"})
*
* @ORM\Column(name="fixed_rate", type="float", nullable=true) * @ORM\Column(name="fixed_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0) * @Assert\GreaterThanOrEqual(0)
*/ */
private $fixedRate = null; private $fixedRate = null;
/** /**
* @var float * @var float
* *
* @Serializer\Expose()
* @Serializer\Groups({"Entity"})
*
* @ORM\Column(name="hourly_rate", type="float", nullable=true) * @ORM\Column(name="hourly_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0) * @Assert\GreaterThanOrEqual(0)
*/ */
private $hourlyRate = null; private $hourlyRate = null;
/** /**
* @var bool * @var bool
* *
* @Serializer\Expose()
* @Serializer\Groups({"Entity"})
*
* @ORM\Column(name="exported", type="boolean", nullable=false) * @ORM\Column(name="exported", type="boolean", nullable=false)
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $exported = false; private $exported = false;
/** /**
* @var bool * @var bool
* *
@@ -180,7 +252,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $billable = true; private $billable = true;
/** /**
* @var string * @var string
* *
@@ -188,16 +259,17 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
* @Assert\NotNull() * @Assert\NotNull()
*/ */
private $category = self::WORK; private $category = self::WORK;
/** /**
* @var DateTime|null * @var DateTime|null
* @internal used for limiting queries, eg. via API sync
* *
* @Gedmo\Timestampable * @Gedmo\Timestampable
* @ORM\Column(name="modified_at", type="datetime", nullable=true) * @ORM\Column(name="modified_at", type="datetime", nullable=true)
*/ */
private $modifiedAt; private $modifiedAt;
/** /**
* Tags
*
* @var Tag[]|ArrayCollection * @var Tag[]|ArrayCollection
* *
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="timesheets", cascade={"persist"}) * @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="timesheets", cascade={"persist"})
@@ -213,10 +285,19 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
* @Assert\Valid() * @Assert\Valid()
*/ */
private $tags; private $tags;
/** /**
* Meta fields
*
* All visible meta (custom) fields registered with this timesheet
*
* @var TimesheetMeta[]|Collection * @var TimesheetMeta[]|Collection
* *
* @Serializer\Expose()
* @Serializer\Groups({"Timesheet"})
* @Serializer\Type(name="array<App\Entity\TimesheetMeta>")
* @Serializer\SerializedName("metaFields")
* @Serializer\Accessor(getter="getVisibleMetaFields")
*
* @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"}) * @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"})
*/ */
private $meta; private $meta;

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -21,6 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\UniqueConstraint(columns={"timesheet_id", "name"}) * @ORM\UniqueConstraint(columns={"timesheet_id", "name"})
* } * }
* ) * )
* @Serializer\ExclusionPolicy("all")
*/ */
class TimesheetMeta implements MetaTableTypeInterface class TimesheetMeta implements MetaTableTypeInterface
{ {

View File

@@ -14,6 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser; use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -28,6 +29,26 @@ use Symfony\Component\Validator\Constraints as Assert;
* ) * )
* @UniqueEntity("username") * @UniqueEntity("username")
* @UniqueEntity("email") * @UniqueEntity("email")
*
* @Serializer\ExclusionPolicy("all")
* @Serializer\VirtualProperty(
* "LanguageAsString",
* exp="object.getLocale()",
* options={
* @Serializer\SerializedName("language"),
* @Serializer\Type(name="string"),
* @Serializer\Groups({"User_Entity"})
* }
* )
* @Serializer\VirtualProperty(
* "TimezoneAsString",
* exp="object.getTimezone()",
* options={
* @Serializer\SerializedName("timezone"),
* @Serializer\Type(name="string"),
* @Serializer\Groups({"User_Entity"})
* }
* )
*/ */
class User extends BaseUser implements UserInterface class User extends BaseUser implements UserInterface
{ {
@@ -44,67 +65,94 @@ class User extends BaseUser implements UserInterface
public const AUTH_SAML = 'saml'; public const AUTH_SAML = 'saml';
/** /**
* Internal ID
*
* @var int * @var int
* @internal must be protected because of parent class
*
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
* *
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
*/ */
protected $id; protected $id;
/** /**
* The user alias will be displayed in the frontend instead of the username
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"Default"})
*
* @ORM\Column(name="alias", type="string", length=60, nullable=true) * @ORM\Column(name="alias", type="string", length=60, nullable=true)
* @Assert\Length(max=60) * @Assert\Length(max=60)
*/ */
private $alias; private $alias;
/** /**
* Registration date for the user
*
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(name="registration_date", type="datetime", nullable=true) * @ORM\Column(name="registration_date", type="datetime", nullable=true)
*/ */
private $registeredAt; private $registeredAt;
/** /**
* An additional title for the user, like the Job position or Department
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"User_Entity"})
*
* @ORM\Column(name="title", type="string", length=50, nullable=true) * @ORM\Column(name="title", type="string", length=50, nullable=true)
* @Assert\Length(max=50) * @Assert\Length(max=50)
*/ */
private $title; private $title;
/** /**
* URL to the users avatar, will be auto-generated if empty
*
* @var string * @var string
* *
* @Serializer\Expose()
* @Serializer\Groups({"User_Entity"})
*
* @ORM\Column(name="avatar", type="string", length=255, nullable=true) * @ORM\Column(name="avatar", type="string", length=255, nullable=true)
* @Assert\Length(max=255) * @Assert\Length(max=255)
*/ */
private $avatar; private $avatar;
/** /**
* API token (password) for this user
*
* @var string * @var string
* *
* @ORM\Column(name="api_token", type="string", length=255, nullable=true) * @ORM\Column(name="api_token", type="string", length=255, nullable=true)
*/ */
protected $apiToken; private $apiToken;
/** /**
* @var string * @var string
* @internal to be set via form, must not be persisted
*/ */
protected $plainApiToken; private $plainApiToken;
/** /**
* User preferences
*
* List of preferences for this user, required ones have dedicated fields/methods
*
* @var UserPreference[]|Collection * @var UserPreference[]|Collection
* *
* @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user", cascade={"persist"}) * @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user", cascade={"persist"})
*/ */
private $preferences; private $preferences;
/** /**
* All teams of the user
*
* @var Team[]|ArrayCollection * @var Team[]|ArrayCollection
* *
* @Serializer\Expose()
* @Serializer\Groups({"User_Entity"})
*
* @ORM\ManyToMany(targetEntity="Team", inversedBy="users", cascade={"persist"}) * @ORM\ManyToMany(targetEntity="Team", inversedBy="users", cascade={"persist"})
* @ORM\JoinTable( * @ORM\JoinTable(
* name="kimai2_users_teams", * name="kimai2_users_teams",
@@ -117,25 +165,24 @@ class User extends BaseUser implements UserInterface
* ) * )
*/ */
private $teams; private $teams;
/** /**
* The type of authentication used by the user (eg. "kimai", "ldap", "saml")
*
* @var string * @var string
* @internal for internal usage only
* *
* @ORM\Column(name="auth", type="string", length=20, nullable=true) * @ORM\Column(name="auth", type="string", length=20, nullable=true)
* @Assert\Length(max=20) * @Assert\Length(max=20)
*/ */
private $auth = self::AUTH_INTERNAL; private $auth = self::AUTH_INTERNAL;
/** /**
* This flag will be initialized in UserEnvironmentSubscriber. * This flag will be initialized in UserEnvironmentSubscriber.
* *
* @var bool|null * @var bool|null
* @internal has no database mapping. as the value is calculated from a permission
*/ */
private $isAllowedToSeeAllData = null; private $isAllowedToSeeAllData = null;
/**
* User constructor.
*/
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();

View File

@@ -27,7 +27,7 @@ abstract class AbstractRateForm extends AbstractType
// documentation is for NelmioApiDocBundle // documentation is for NelmioApiDocBundle
'documentation' => [ 'documentation' => [
'type' => 'number', 'type' => 'number',
'description' => 'Rate', 'description' => 'The rate (eg. 10.5)',
], ],
'label' => 'label.rate', 'label' => 'label.rate',
'attr' => [ 'attr' => [
@@ -40,7 +40,7 @@ abstract class AbstractRateForm extends AbstractType
// documentation is for NelmioApiDocBundle // documentation is for NelmioApiDocBundle
'documentation' => [ 'documentation' => [
'type' => 'number', 'type' => 'number',
'description' => 'Internal rate', 'description' => 'The internal rate (eg. 10.0 or 10)',
], ],
'label' => 'label.rate_internal', 'label' => 'label.rate_internal',
'currency' => $currency, 'currency' => $currency,
@@ -50,6 +50,10 @@ abstract class AbstractRateForm extends AbstractType
->add('isFixed', YesNoType::class, [ ->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate', 'label' => 'label.fixedRate',
'help' => 'help.fixedRate', 'help' => 'help.fixedRate',
'documentation' => [
'type' => 'boolean',
'description' => 'If "true" each time record gets the same rate, regardless of its duration',
],
]) ])
; ;
} }

View File

@@ -29,6 +29,10 @@ class TagEditForm extends AbstractType
'attr' => [ 'attr' => [
'autofocus' => 'autofocus' 'autofocus' => 'autofocus'
], ],
'documentation' => [
'type' => 'string',
'description' => 'The tag name (forbidden character: comma)',
],
]) ])
->add('color', ColorPickerType::class); ->add('color', ColorPickerType::class);
} }

View File

@@ -32,7 +32,7 @@ class TeamEditForm extends AbstractType
// documentation is for NelmioApiDocBundle // documentation is for NelmioApiDocBundle
'documentation' => [ 'documentation' => [
'type' => 'string', 'type' => 'string',
'description' => 'Name of the new team', 'description' => 'Name of the team',
], ],
]) ])
->add('teamlead', UserType::class, [ ->add('teamlead', UserType::class, [
@@ -49,6 +49,12 @@ class TeamEditForm extends AbstractType
'multiple' => true, 'multiple' => true,
'expanded' => $options['expand_users'], 'expanded' => $options['expand_users'],
'by_reference' => false, 'by_reference' => false,
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'User IDs'],
'title' => 'Team member',
'description' => 'Array of team member IDs',
],
]) ])
; ;
} }

View File

@@ -33,6 +33,10 @@ class ColorPickerType extends AbstractType implements DataTransformerInterface
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'documentation' => [
'type' => 'string',
'description' => sprintf('The color code as hex (default: %s)', self::DEFAULT_COLOR),
],
'label' => 'label.color', 'label' => 'label.color',
]); ]);
} }

View File

@@ -34,6 +34,10 @@ class UserType extends AbstractType
return $user->getDisplayName(); return $user->getDisplayName();
}, },
'choice_translation_domain' => false, 'choice_translation_domain' => false,
'documentation' => [
'type' => 'integer',
'description' => 'User ID',
],
]); ]);
$resolver->setDefault('query_builder', function (Options $options) { $resolver->setDefault('query_builder', function (Options $options) {

View File

@@ -56,6 +56,7 @@ class UserCreateType extends UserEditType
parent::configureOptions($resolver); parent::configureOptions($resolver);
$resolver->setDefaults([ $resolver->setDefaults([
'validation_groups' => ['UserCreate'],
'include_add_more' => false, 'include_add_more' => false,
]); ]);
} }

View File

@@ -72,6 +72,7 @@ class UserEditType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'validation_groups' => ['Profile'],
'data_class' => User::class, 'data_class' => User::class,
'csrf_protection' => true, 'csrf_protection' => true,
'csrf_field_name' => '_token', 'csrf_field_name' => '_token',

View File

@@ -41,6 +41,7 @@ class UserPasswordType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'validation_groups' => ['PasswordUpdate'],
'data_class' => User::class, 'data_class' => User::class,
'csrf_protection' => true, 'csrf_protection' => true,
'csrf_field_name' => '_token', 'csrf_field_name' => '_token',

View File

@@ -40,6 +40,7 @@ class UserRolesType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'validation_groups' => ['RolesUpdate'],
'data_class' => User::class, 'data_class' => User::class,
'csrf_protection' => true, 'csrf_protection' => true,
'csrf_field_name' => '_token', 'csrf_field_name' => '_token',

View File

@@ -12,6 +12,7 @@ namespace App\Tests\API;
use App\DataFixtures\UserFixtures; use App\DataFixtures\UserFixtures;
use App\Entity\User; use App\Entity\User;
use App\Tests\Controller\ControllerBaseTest; use App\Tests\Controller\ControllerBaseTest;
use PHPUnit\Framework\Constraint\IsType;
use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\HttpKernelBrowser;
@@ -240,7 +241,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
/** /**
* @param Response $response * @param Response $response
* @param string[] $failedFields * @param string[] $failedFields
* @param bool $extraFields * @param bool $extraFields test for the error "This form should not contain extra fields"
*/ */
protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false) protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false)
{ {
@@ -258,8 +259,366 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
$data = $result['errors']['children']; $data = $result['errors']['children'];
foreach ($failedFields as $fieldName) { foreach ($failedFields as $fieldName) {
self::assertArrayHasKey($fieldName, $data); self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field: %s', $fieldName));
self::assertArrayHasKey('errors', $data[$fieldName]); self::assertArrayHasKey('errors', $data[$fieldName], sprintf('Field %s has no validation problem', $fieldName));
}
$foundErrors = [];
foreach ($data as $fieldName => $field) {
if (\array_key_exists('errors', $field) && \count($field['errors']) > 0) {
$foundErrors[$fieldName] = \count($field['errors']);
}
}
self::assertEquals(\count($failedFields), \count($foundErrors), 'Expected and actual validation error amount differs');
}
protected static function getExpectedResponseStructure(string $type): array
{
switch ($type) {
case 'TagEntity':
return [
'id' => 'int',
'name' => 'string',
'color' => 'string',
];
// embedded meta data
case 'CustomerMeta':
case 'ProjectMeta':
case 'ActivityMeta':
case 'TimesheetMeta':
return [
'name' => 'string',
'value' => 'string',
];
// if a user is embedded in other objects
case 'User':
// if a list of users is loaded
case 'UserCollection':
return [
'id' => 'int',
'username' => 'string',
'enabled' => 'bool',
'alias' => '@string',
];
// if a user is loaded explicitly
case 'UserEntity':
return [
'id' => 'int',
'username' => 'string',
'enabled' => 'bool',
'alias' => '@string',
'title' => '@string',
'avatar' => '@string',
'teams' => ['result' => 'array', 'type' => 'Team'],
'roles' => ['result' => 'array', 'type' => 'string'],
'language' => 'string',
'timezone' => 'string',
];
// if a team is embedded
case 'Team':
// if a collection of teams is requested
case 'TeamCollection':
return [
'id' => 'int',
'name' => 'string',
];
// explicitly requested team
case 'TeamEntity':
return [
'id' => 'int',
'name' => 'string',
'teamlead' => ['result' => 'object', 'type' => 'User'],
'users' => ['result' => 'array', 'type' => 'User'],
'customers' => ['result' => 'array', 'type' => '@Customer'],
'projects' => ['result' => 'array', 'type' => '@Project'],
];
// if a customer is embedded in other objects
case 'Customer':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'color' => '@string',
];
// if a list of customers is loaded
case 'CustomerCollection':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'boolean',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
'teams' => ['result' => 'array', 'type' => 'Team'],
'currency' => 'string', // since 1.10
];
// if a customer is loaded explicitly
case 'CustomerEntity':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
'teams' => ['result' => 'array', 'type' => 'Team'],
'homepage' => '@string',
'number' => '@string',
'comment' => '@string',
'company' => '@string',
'contact' => '@string',
'address' => '@string',
'country' => 'string',
'currency' => 'string',
'phone' => '@string',
'fax' => '@string',
'mobile' => '@string',
'email' => '@string',
'timezone' => 'string',
'budget' => 'float',
'timeBudget' => 'int',
'vatId' => '@string', // since 1.10
];
// if a project is embedded
case 'Project':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'color' => '@string',
'customer' => 'int',
];
// if a project is embedded in an expanded collection (here timesheet)
case 'ProjectExpanded':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'color' => '@string',
'customer' => ['result' => 'object', 'type' => 'Customer'],
];
// if a collection of projects is loaded
case 'ProjectCollection':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'customer' => 'int',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'parentTitle' => 'string',
'start' => '@datetime',
'end' => '@datetime',
'teams' => ['result' => 'array', 'type' => 'Team'],
];
// if a project is explicitly loaded
case 'ProjectEntity':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'customer' => 'int',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'parentTitle' => 'string',
'start' => '@datetime',
'end' => '@datetime',
'teams' => ['result' => 'array', 'type' => 'Team'],
'comment' => '@string',
'budget' => 'float',
'timeBudget' => 'int',
'orderNumber' => '@string',
'orderDate' => '@datetime',
];
// embedded activities
case 'Activity':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'project' => '@int',
'color' => '@string',
];
// collection of activities
case 'ActivityCollection':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'project' => '@int',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'parentTitle' => '@string',
];
// if a activity is explicitly loaded
case 'ActivityEntity':
return [
'id' => 'int',
'name' => 'string',
'visible' => 'bool',
'project' => '@int',
'color' => '@string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'parentTitle' => '@string',
'comment' => '@string',
'budget' => 'float',
'timeBudget' => 'int',
];
case 'TimesheetEntity':
return [
'id' => 'int',
'begin' => 'DateTime',
'end' => '@DateTime',
'duration' => '@int',
'description' => '@string',
'rate' => 'float',
'activity' => 'int',
'project' => 'int',
'tags' => ['result' => 'array', 'type' => 'string'],
'user' => 'int',
'metaFields' => ['result' => 'array', 'type' => 'TimesheetMeta'],
'internalRate' => 'float',
'exported' => 'bool',
'fixedRate' => '@float',
'hourlyRate' => '@float',
// TODO new fields: billable, category
];
case 'TimesheetCollection':
return [
'id' => 'int',
'begin' => 'DateTime',
'end' => '@DateTime',
'duration' => '@int',
'description' => '@string',
'rate' => 'float',
'activity' => 'int',
'project' => 'int',
'tags' => ['result' => 'array', 'type' => 'string'],
'user' => 'int',
'metaFields' => ['result' => 'array', 'type' => 'TimesheetMeta'],
'internalRate' => 'float',
];
case 'TimesheetCollectionFull':
return [
'id' => 'int',
'begin' => 'DateTime',
'end' => '@DateTime',
'duration' => '@int',
'description' => '@string',
'rate' => 'float',
'activity' => ['result' => 'object', 'type' => 'Activity'],
'project' => ['result' => 'object', 'type' => 'ProjectExpanded'],
'tags' => ['result' => 'array', 'type' => 'string'],
'user' => 'int',
'metaFields' => ['result' => 'array', 'type' => 'TimesheetMeta'],
'internalRate' => 'float',
];
default:
throw new \Exception(sprintf('Unknown API response type: %s', $type));
}
}
/**
* The $type is either one of the types configured in config/packages/nelmio_api_doc.yaml or the class name.
*
* @param string $type
* @param array $result
* @throws \Exception
*/
protected function assertApiResponseTypeStructure(string $type, array $result)
{
$expected = self::getExpectedResponseStructure($type);
$expectedKeys = array_keys($expected);
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
self::assertEquals($expectedKeys, $actual, sprintf('Structure for API response type "%s" does not match', $type));
self::assertEquals(
\count($actual),
\count($expectedKeys),
sprintf('Mismatch between expected and result keys for API response type "%s". Expected %s keys but found %s.', $type, \count($expected), \count($actual))
);
foreach ($expected as $key => $value) {
if (\is_array($value)) {
switch ($value['result']) {
case 'array':
foreach ($result[$key] as $subResult) {
if ($value['type'] === 'string') {
self::assertIsString($subResult);
} else {
self::assertIsArray($subResult);
if ($value['type'][0] === '@') {
if (empty($result[$key])) {
continue;
}
$value['type'] = substr($value['type'], 1);
}
self::assertApiResponseTypeStructure($value['type'], $subResult);
}
}
break;
case 'object':
self::assertIsArray($result[$key], sprintf('Key "%s" in type "%s" is not an array', $key, $type));
if ($value['type'][0] === '@') {
if (empty($result[$key])) {
break;
}
$value['type'] = substr($value['type'], 1);
}
self::assertApiResponseTypeStructure($value['type'], $result[$key]);
break;
default:
throw new \Exception(sprintf('Invalid result type "%s" for subresource given', $value['result']));
}
continue;
}
if ($value[0] === '@') {
if (\is_null($result[$key])) {
continue;
}
$value = substr($value, 1);
}
if (strtolower($value) === 'datetime') {
// TODO
$value = 'string';
}
static::assertThat(
$result[$key],
new IsType($value),
sprintf('Found type mismatch in structure for API response type %s. Expected type "%s" for key "%s".', $type, $value, $key)
);
} }
} }
} }

View File

@@ -11,6 +11,7 @@ namespace App\Tests\API;
use App\DataFixtures\UserFixtures; use App\DataFixtures\UserFixtures;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\ActivityRate; use App\Entity\ActivityRate;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
@@ -103,6 +104,12 @@ class ActivityControllerTest extends APIControllerBaseTest
$em->persist($activity); $em->persist($activity);
$activity = (new Activity())->setName('fifth one')->setComment('5')->setProject($project2); $activity = (new Activity())->setName('fifth one')->setComment('5')->setProject($project2);
$meta = new ActivityMeta();
$meta->setName('bar')->setValue('foo')->setIsVisible(false);
$activity->setMetaField($meta);
$meta = new ActivityMeta();
$meta->setName('foo')->setValue('bar')->setIsVisible(true);
$activity->setMetaField($meta);
$em->persist($activity); $em->persist($activity);
$activity = (new Activity())->setName('sixth one')->setComment('6')->setVisible(false); $activity = (new Activity())->setName('sixth one')->setComment('6')->setVisible(false);
@@ -127,7 +134,7 @@ class ActivityControllerTest extends APIControllerBaseTest
for ($i = 0; $i < \count($result); $i++) { for ($i = 0; $i < \count($result); $i++) {
$activity = $result[$i]; $activity = $result[$i];
$hasProject = $expected[$i][0]; $hasProject = $expected[$i][0];
$this->assertStructure($activity, false); self::assertApiResponseTypeStructure('ActivityCollection', $activity);
if ($hasProject) { if ($hasProject) {
$this->assertEquals($expected[$i][1], $activity['project']); $this->assertEquals($expected[$i][1], $activity['project']);
} }
@@ -161,7 +168,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(5, \count($result)); $this->assertEquals(5, \count($result));
$this->assertStructure($result[0], false); self::assertApiResponseTypeStructure('ActivityCollection', $result[0]);
$this->assertEquals(1, $result[4]['project']); $this->assertEquals(1, $result[4]['project']);
$this->assertEquals(2, $result[3]['project']); $this->assertEquals(2, $result[3]['project']);
$this->assertEquals(2, $result[2]['project']); $this->assertEquals(2, $result[2]['project']);
@@ -174,7 +181,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result, true); self::assertApiResponseTypeStructure('ActivityEntity', $result);
} }
public function testNotFound() public function testNotFound()
@@ -197,7 +204,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -212,7 +219,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -262,7 +269,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -351,23 +358,4 @@ class ActivityControllerTest extends APIControllerBaseTest
$activity = $em->getRepository(Activity::class)->find(1); $activity = $em->getRepository(Activity::class)->find(1);
$this->assertEquals('another,testing,bar', $activity->getMetaField('metatestmock')->getValue()); $this->assertEquals('another,testing,bar', $activity->getMetaField('metatestmock')->getValue());
} }
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'visible', 'project', 'color', 'metaFields', 'parentTitle'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'comment', 'budget', 'timeBudget'
]);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Activity structure does not match');
}
} }

View File

@@ -27,6 +27,24 @@ class ApiDocControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/doc'); $this->assertAccessIsGranted($client, '/api/doc');
$this->assertStringContainsString('<title>Kimai 2 - API Docs</title>', $client->getResponse()->getContent()); $this->assertStringContainsString('<title>Kimai 2 - API Docs</title>', $client->getResponse()->getContent());
$result = $client->getCrawler()->filter('script#swagger-data');
$swaggerJson = json_decode($result->text(), true);
$tags = [];
foreach ($swaggerJson['spec']['paths'] as $path) {
foreach ($path as $method) {
foreach ($method['tags'] as $tag) {
$tags[$tag] = $tag;
}
}
}
$expectedKeys = ['Activity', 'Default', 'Customer', 'Project', 'Tag', 'Team', 'Timesheet', 'User'];
$actual = array_keys($tags);
sort($actual);
sort($expectedKeys);
self::assertEquals($expectedKeys, $actual, sprintf('Expected %s sections in API docs, but found %s.', \count($actual), \count($expectedKeys)));
} }
public function testGetJsonDocs() public function testGetJsonDocs()

View File

@@ -10,8 +10,12 @@
namespace App\Tests\API; namespace App\Tests\API;
use App\DataFixtures\UserFixtures; use App\DataFixtures\UserFixtures;
use App\Entity\Activity;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\CustomerMeta;
use App\Entity\CustomerRate; use App\Entity\CustomerRate;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User; use App\Entity\User;
use App\Repository\CustomerRateRepository; use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
@@ -84,12 +88,12 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
$this->assertStructure($result[0], false); self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
} }
public function testGetCollectionWithQuery() public function testGetCollectionWithQuery()
{ {
$query = ['order' => 'ASC', 'orderBy' => 'name', 'visible' => 3]; $query = ['order' => 'ASC', 'orderBy' => 'name', 'visible' => 3, 'term' => 'test'];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/customers', 'GET', $query); $this->assertAccessIsGranted($client, '/api/customers', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
@@ -97,7 +101,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
$this->assertStructure($result[0], false); self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
} }
public function testGetEntity() public function testGetEntity()
@@ -107,7 +111,51 @@ class CustomerControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result, true); self::assertApiResponseTypeStructure('CustomerEntity', $result);
}
public function testGetEntityWithFullResponse()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
/** @var Customer $customer */
$customer = $em->getRepository(Customer::class)->find(1);
// add meta fields
$meta = new CustomerMeta();
$meta->setName('bar')->setValue('foo')->setIsVisible(false);
$customer->setMetaField($meta);
$meta = new CustomerMeta();
$meta->setName('foo')->setValue('bar')->setIsVisible(true);
$customer->setMetaField($meta);
$em->persist($customer);
// add a new project ...
$project = new Project();
$project->setName('Activity Test');
$project->setCustomer($customer);
$em->persist($project);
// ... with activity
$activity = (new Activity())->setName('first one')->setComment('1')->setProject($project);
$em->persist($activity);
// and finally a team
$team = new Team();
$team->setName('Testing customer 1 team');
$team->setTeamLead($this->getUserByRole(User::ROLE_USER));
$team->addCustomer($customer);
$team->addProject($project);
$team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD));
$em->persist($team);
$this->assertAccessIsGranted($client, '/api/customers/1');
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
} }
public function testNotFound() public function testNotFound()
@@ -132,7 +180,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -150,7 +198,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -206,7 +254,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -252,6 +300,13 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertApiCallValidationError($response, ['currency']); $this->assertApiCallValidationError($response, ['currency']);
} }
public function testMetaActionNotAllowed()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/api/customers/1/meta', 'PATCH', [], json_encode(['name' => 'asdasd']));
$this->assertApiResponseAccessDenied($client->getResponse(), 'You are not allowed to update this customer');
}
public function testMetaActionThrowsNotFound() public function testMetaActionThrowsNotFound()
{ {
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/customers/42/meta', []); $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/customers/42/meta', []);
@@ -299,24 +354,4 @@ class CustomerControllerTest extends APIControllerBaseTest
$customer = $em->getRepository(Customer::class)->find(1); $customer = $em->getRepository(Customer::class)->find(1);
$this->assertEquals('another,testing,bar', $customer->getMetaField('metatestmock')->getValue()); $this->assertEquals('another,testing,bar', $customer->getMetaField('metatestmock')->getValue());
} }
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'visible', 'color', 'metaFields', 'teams'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'homepage', 'number', 'comment', 'company', 'contact', 'address', 'country', 'currency',
'phone', 'fax', 'mobile', 'email', 'timezone', 'budget', 'timeBudget'
]);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Customer structure does not match');
}
} }

View File

@@ -0,0 +1,32 @@
<?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\API\Model;
use App\API\Model\I18nConfig;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\API\Model\I18nConfig
*/
class I18nConfigTest extends TestCase
{
public function testSetter()
{
$sut = new I18nConfig();
$this->assertInstanceOf(I18nConfig::class, $sut->setIs24hours(false));
$this->assertInstanceOf(I18nConfig::class, $sut->setDuration('foo'));
$this->assertInstanceOf(I18nConfig::class, $sut->setDate('bar'));
$this->assertInstanceOf(I18nConfig::class, $sut->setDateTime('hello'));
$this->assertInstanceOf(I18nConfig::class, $sut->setFormDate('world'));
$this->assertInstanceOf(I18nConfig::class, $sut->setFormDateTime('testing'));
$this->assertInstanceOf(I18nConfig::class, $sut->setTime('fun'));
}
}

View File

@@ -1,52 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\API\Model;
use App\API\Model\I18n;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\API\Model\I18n
*/
class I18nTest extends TestCase
{
public function testDefaultValues()
{
$sut = new I18n();
$this->assertTrue($sut->isIs24hours());
$this->assertEquals('', $sut->getDuration());
$this->assertEquals('', $sut->getDate());
$this->assertEquals('', $sut->getDateTime());
$this->assertEquals('', $sut->getFormDate());
$this->assertEquals('', $sut->getFormDateTime());
$this->assertEquals('', $sut->getTime());
}
public function testSetter()
{
$sut = new I18n();
$this->assertInstanceOf(I18n::class, $sut->setIs24hours(false));
$this->assertInstanceOf(I18n::class, $sut->setDuration('foo'));
$this->assertInstanceOf(I18n::class, $sut->setDate('bar'));
$this->assertInstanceOf(I18n::class, $sut->setDateTime('hello'));
$this->assertInstanceOf(I18n::class, $sut->setFormDate('world'));
$this->assertInstanceOf(I18n::class, $sut->setFormDateTime('testing'));
$this->assertInstanceOf(I18n::class, $sut->setTime('fun'));
$this->assertFalse($sut->isIs24hours());
$this->assertEquals('foo', $sut->getDuration());
$this->assertEquals('bar', $sut->getDate());
$this->assertEquals('hello', $sut->getDateTime());
$this->assertEquals('world', $sut->getFormDate());
$this->assertEquals('testing', $sut->getFormDateTime());
$this->assertEquals('fun', $sut->getTime());
}
}

View File

@@ -17,17 +17,6 @@ use PHPUnit\Framework\TestCase;
*/ */
class TimesheetConfigTest extends TestCase class TimesheetConfigTest extends TestCase
{ {
public function testDefaultValues()
{
$sut = new TimesheetConfig();
$this->assertTrue($sut->isAllowFutureTimes());
$this->assertTrue($sut->isAllowOverlapping());
$this->assertEquals('now', $sut->getDefaultBeginTime());
$this->assertEquals('default', $sut->getTrackingMode());
$this->assertEquals(1, $sut->getActiveEntriesSoftLimit());
$this->assertEquals(1, $sut->getActiveEntriesHardLimit());
}
public function testSetter() public function testSetter()
{ {
$sut = new TimesheetConfig(); $sut = new TimesheetConfig();
@@ -38,12 +27,5 @@ class TimesheetConfigTest extends TestCase
$this->assertInstanceOf(TimesheetConfig::class, $sut->setTrackingMode('punch')); $this->assertInstanceOf(TimesheetConfig::class, $sut->setTrackingMode('punch'));
$this->assertInstanceOf(TimesheetConfig::class, $sut->setActiveEntriesSoftLimit(2)); $this->assertInstanceOf(TimesheetConfig::class, $sut->setActiveEntriesSoftLimit(2));
$this->assertInstanceOf(TimesheetConfig::class, $sut->setActiveEntriesHardLimit(3)); $this->assertInstanceOf(TimesheetConfig::class, $sut->setActiveEntriesHardLimit(3));
$this->assertFalse($sut->isAllowFutureTimes());
$this->assertFalse($sut->isAllowOverlapping());
$this->assertEquals('08:00', $sut->getDefaultBeginTime());
$this->assertEquals('punch', $sut->getTrackingMode());
$this->assertEquals(2, $sut->getActiveEntriesSoftLimit());
$this->assertEquals(3, $sut->getActiveEntriesHardLimit());
} }
} }

View File

@@ -12,7 +12,9 @@ namespace App\Tests\API;
use App\DataFixtures\UserFixtures; use App\DataFixtures\UserFixtures;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\ProjectMeta;
use App\Entity\ProjectRate; use App\Entity\ProjectRate;
use App\Entity\Team;
use App\Entity\User; use App\Entity\User;
use App\Repository\ProjectRateRepository; use App\Repository\ProjectRateRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
@@ -86,7 +88,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
$this->assertStructure($result[0], false); self::assertApiResponseTypeStructure('ProjectCollection', $result[0]);
} }
protected function loadProjectTestData(HttpKernelBrowser $client) protected function loadProjectTestData(HttpKernelBrowser $client)
@@ -114,8 +116,25 @@ class ProjectControllerTest extends APIControllerBaseTest
$em->persist($project); $em->persist($project);
$project = (new Project())->setName('fifth')->setVisible(true)->setCustomer($customer); $project = (new Project())->setName('fifth')->setVisible(true)->setCustomer($customer);
// add meta fields
$meta = new ProjectMeta();
$meta->setName('bar')->setValue('foo')->setIsVisible(false);
$project->setMetaField($meta);
$meta = new ProjectMeta();
$meta->setName('foo')->setValue('bar')->setIsVisible(true);
$project->setMetaField($meta);
$em->persist($project); $em->persist($project);
// and a team
$team = new Team();
$team->setName('Testing project team');
$team->setTeamLead($this->getUserByRole(User::ROLE_USER));
$team->addCustomer($customer);
$team->addProject($project);
$team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD));
$em->persist($team);
$project = (new Project())->setName('sixth')->setVisible(false)->setCustomer($customer3); $project = (new Project())->setName('sixth')->setVisible(false)->setCustomer($customer3);
$em->persist($project); $em->persist($project);
@@ -138,7 +157,7 @@ class ProjectControllerTest extends APIControllerBaseTest
for ($i = 0; $i < \count($expected); $i++) { for ($i = 0; $i < \count($expected); $i++) {
$project = $result[$i]; $project = $result[$i];
$compare = $expected[$i]; $compare = $expected[$i];
$this->assertStructure($project, false); self::assertApiResponseTypeStructure('ProjectCollection', $project);
$this->assertEquals($compare[1], $project['customer']); $this->assertEquals($compare[1], $project['customer']);
} }
} }
@@ -169,7 +188,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ProjectEntity', $result);
} }
public function testNotFound() public function testNotFound()
@@ -195,7 +214,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertEquals('2018-02-08T13:02:54+0000', $result['orderDate']); self::assertEquals('2018-02-08T13:02:54+0000', $result['orderDate']);
self::assertEquals('2019-02-01T19:32:17+0000', $result['start']); self::assertEquals('2019-02-01T19:32:17+0000', $result['start']);
@@ -214,7 +233,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertEquals('foo', $result['name']); self::assertEquals('foo', $result['name']);
} }
@@ -265,7 +284,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -354,23 +373,4 @@ class ProjectControllerTest extends APIControllerBaseTest
$project = $em->getRepository(Project::class)->find(1); $project = $em->getRepository(Project::class)->find(1);
$this->assertEquals('another,testing,bar', $project->getMetaField('metatestmock')->getValue()); $this->assertEquals('another,testing,bar', $project->getMetaField('metatestmock')->getValue());
} }
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'visible', 'customer', 'color', 'metaFields', 'parentTitle', 'start', 'end', 'teams'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'comment', 'budget', 'timeBudget', 'orderNumber', 'orderDate'
]);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Project structure does not match');
}
} }

View File

@@ -192,6 +192,15 @@ trait RateControllerTestTrait
$this->assertNotFoundForDelete($client, $this->getRateUrl(2, 1)); $this->assertNotFoundForDelete($client, $this->getRateUrl(2, 1));
} }
public function testDeleteNotAllowed()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importTestRates(1);
$this->request($client, $this->getRateUrl(1, 1), 'DELETE');
$this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.');
}
protected function assertRateStructure(array $result, $user = null) protected function assertRateStructure(array $result, $user = null)
{ {
$expectedKeys = [ $expectedKeys = [

View File

@@ -66,14 +66,30 @@ class TagControllerTest extends APIControllerBaseTest
$this->importTagFixtures($client); $this->importTagFixtures($client);
$data = [ $data = [
'name' => 'foo', 'name' => 'foo',
'color' => '#000FFF'
]; ];
$this->request($client, '/api/tags', 'POST', [], json_encode($data)); $this->request($client, '/api/tags', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TagEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertEquals('#000FFF', $result['color']);
}
public function testPostActionWithValidationErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importTagFixtures($client);
$data = [
'name' => '1',
'color' => '11231231231',
];
$this->request($client, '/api/tags', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['name', 'color']);
} }
public function testPostActionWithInvalidUser() public function testPostActionWithInvalidUser()
@@ -128,21 +144,4 @@ class TagControllerTest extends APIControllerBaseTest
{ {
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255'); $this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255');
} }
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'color'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, []);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Tag structure does not match');
}
} }

View File

@@ -59,7 +59,7 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
self::assertEquals(2, \count($result)); self::assertEquals(2, \count($result));
$this->assertStructure($result[0], false); self::assertApiResponseTypeStructure('TeamCollection', $result[0]);
} }
public function testGetEntity() public function testGetEntity()
@@ -70,7 +70,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result, true); self::assertApiResponseTypeStructure('TeamEntity', $result);
} }
public function testNotFound() public function testNotFound()
@@ -95,7 +95,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
} }
@@ -114,6 +114,20 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertEquals('Access denied.', $json['message']); self::assertEquals('Access denied.', $json['message']);
} }
public function testPostActionWithValidationErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => '',
'teamlead' => 9999,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['name', 'teamlead']);
}
public function testPatchAction() public function testPatchAction()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -135,11 +149,34 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertCount(4, $result['users']); self::assertCount(4, $result['users']);
} }
public function testPatchActionWithValidationErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$data = [
'name' => '1',
'teamlead' => 9999,
'users' => [9999]
];
$this->request($client, '/api/teams/' . $result['id'], 'PATCH', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['name', 'teamlead', 'users']);
}
public function testDeleteAction() public function testDeleteAction()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -148,7 +185,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$id = $result['id']; $id = $result['id'];
@@ -175,7 +212,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(2, $result['users']); self::assertCount(2, $result['users']);
} }
@@ -238,7 +275,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(3, $result['users']); self::assertCount(3, $result['users']);
} }
@@ -300,7 +337,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(1, $result['customers']); self::assertCount(1, $result['customers']);
self::assertEquals(1, $result['customers'][0]['id']); self::assertEquals(1, $result['customers'][0]['id']);
} }
@@ -381,7 +418,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(0, $result['customers']); self::assertCount(0, $result['customers']);
} }
@@ -427,13 +464,12 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(0, $result['projects']); self::assertCount(0, $result['projects']);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(1, $result['projects']); self::assertCount(1, $result['projects']);
self::assertEquals(1, $result['projects'][0]['id']); self::assertEquals(1, $result['projects'][0]['id']);
} }
@@ -520,7 +556,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('TeamEntity', $result);
self::assertCount(0, $result['projects']); self::assertCount(0, $result['projects']);
} }
@@ -554,23 +590,4 @@ class TeamControllerTest extends APIControllerBaseTest
$json = json_decode($client->getResponse()->getContent(), true); $json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Project is not assigned to the team', $json['message']); self::assertEquals('Project is not assigned to the team', $json['message']);
} }
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'teamlead', 'users', 'customers', 'projects'
]);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
self::assertEquals($expectedKeys, $actual, 'Team structure does not match');
}
} }

View File

@@ -59,7 +59,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testGetCollectionFull() public function testGetCollectionFull()
@@ -72,8 +72,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
$this->assertHasSubresources($result[0]);
} }
public function testGetCollectionForOtherUser() public function testGetCollectionForOtherUser()
@@ -99,14 +98,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testGetCollectionForAllUser() public function testGetCollectionForAllUser()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures(); $fixture = new TimesheetFixtures();
$fixture $fixture
@@ -125,7 +123,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(17, \count($result)); $this->assertEquals(17, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testGetCollectionForEmptyResult() public function testGetCollectionForEmptyResult()
@@ -170,7 +168,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(5, \count($result)); $this->assertEquals(5, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testGetCollectionWithSingleParamsQuery() public function testGetCollectionWithSingleParamsQuery()
@@ -202,14 +200,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(5, \count($result)); $this->assertEquals(5, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testExportedFilter() public function testExportedFilter()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures(); $fixture = new TimesheetFixtures();
$fixture $fixture
@@ -240,7 +237,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(7, \count($result)); $this->assertEquals(7, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [ $query = [
'page' => 1, 'page' => 1,
@@ -256,7 +253,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [ $query = [
'page' => 1, 'page' => 1,
@@ -270,7 +267,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(17, \count($result)); $this->assertEquals(17, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testGetEntity() public function testGetEntity()
@@ -281,7 +278,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
} }
public function testGetEntityAccessDenied() public function testGetEntityAccessDenied()
@@ -301,7 +298,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
} }
public function testGetEntityNotFound() public function testGetEntityNotFound()
@@ -327,7 +324,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied $this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']); $this->assertEquals(2016, $result['rate']);
@@ -357,7 +354,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$this->assertEquals($user->getId(), $result['user']); $this->assertEquals($user->getId(), $result['user']);
$this->assertNotEquals($admin->getId(), $result['user']); $this->assertNotEquals($admin->getId(), $result['user']);
@@ -437,7 +434,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$this->assertEquals(25200, $result['duration']); $this->assertEquals(25200, $result['duration']);
$this->assertEquals(1, $result['exported']); $this->assertEquals(1, $result['exported']);
@@ -447,7 +444,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures(); $fixture = new TimesheetFixtures();
$fixture $fixture
@@ -510,7 +506,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$id = $result['id']; $id = $result['id'];
@@ -585,10 +581,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
} }
public function testGetRecentCollectionWithSubresources() public function testGetRecentAction()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days'); $start = new \DateTime('-10 days');
@@ -614,14 +609,12 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
$this->assertHasSubresources($result[0]);
} }
public function testActiveAction() public function testActiveAction()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days'); $start = new \DateTime('-10 days');
@@ -642,7 +635,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$results = json_decode($client->getResponse()->getContent(), true); $results = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals(3, \count($results)); $this->assertEquals(3, \count($results));
foreach ($results as $timesheet) { foreach ($results as $timesheet) {
$this->assertDefaultStructure($timesheet, false); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet);
} }
} }
@@ -650,7 +643,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days'); $start = new \DateTime('-10 days');
@@ -668,6 +660,11 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/11/stop', 'PATCH'); $this->request($client, '/api/timesheets/11/stop', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em = $this->getEntityManager(); $em = $this->getEntityManager();
/** @var Timesheet $timesheet */ /** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1); $timesheet = $em->getRepository(Timesheet::class)->find(1);
@@ -692,7 +689,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days'); $start = new \DateTime('-10 days');
@@ -715,7 +711,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$em = $this->getEntityManager();
$fixture = new TimesheetFixtures(); $fixture = new TimesheetFixtures();
$fixture $fixture
@@ -736,7 +731,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(5, \count($result)); $this->assertEquals(5, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => 'Test,Admin']; $query = ['tags' => 'Test,Admin'];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
@@ -745,7 +740,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => 'Nothing-2-see,here']; $query = ['tags' => 'Nothing-2-see,here'];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
@@ -754,7 +749,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(20, \count($result)); $this->assertEquals(20, \count($result));
$this->assertDefaultStructure($result[0], false); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
public function testRestartAction() public function testRestartAction()
@@ -772,7 +767,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertDefaultStructure($result, true); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']); $this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']); $this->assertEmpty($result['tags']);
@@ -812,7 +807,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertDefaultStructure($result, true); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEquals('foo', $result['description']); $this->assertEquals('foo', $result['description']);
$this->assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']); $this->assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
$this->assertEquals(['another', 'testing', 'bar'], $result['tags']); $this->assertEquals(['another', 'testing', 'bar'], $result['tags']);
@@ -832,8 +827,6 @@ class TimesheetControllerTest extends APIControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
$start = new \DateTime('-10 days'); $start = new \DateTime('-10 days');
$fixture = new TimesheetFixtures(); $fixture = new TimesheetFixtures();
@@ -874,7 +867,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied $this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']); $this->assertEquals(2016, $result['rate']);
@@ -884,7 +877,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertDefaultStructure($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied $this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']); $this->assertEquals(2016, $result['rate']);
@@ -907,7 +900,8 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/1/export', 'PATCH'); $this->request($client, '/api/timesheets/1/export', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->assertDefaultStructure(json_decode($client->getResponse()->getContent(), true), true); $result = json_decode($client->getResponse()->getContent(), true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em->clear(); $em->clear();
/** @var Timesheet $timesheet */ /** @var Timesheet $timesheet */
@@ -988,46 +982,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]);
$em = $this->getEntityManager(); $em = $this->getEntityManager();
/** @var Timesheet $timesheet */ /** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1); $timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue()); $this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue());
} }
protected function assertDefaultStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'begin', 'end', 'duration', 'description', 'rate', 'activity', 'project', 'tags', 'user', 'metaFields', 'internalRate'
];
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'exported', 'fixedRate', 'hourlyRate'
]);
}
$actual = array_keys($result);
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Timesheet structure does not match');
}
protected function assertHasSubresources(array $result)
{
$this->assertArrayHasKey('activity', $result);
$this->assertArrayHasKey('id', $result['activity']);
$this->assertArrayHasKey('name', $result['activity']);
$this->assertArrayHasKey('visible', $result['activity']);
$this->assertArrayHasKey('project', $result['activity']);
$this->assertArrayHasKey('project', $result);
$this->assertArrayHasKey('id', $result['project']);
$this->assertArrayHasKey('name', $result['project']);
$this->assertArrayHasKey('visible', $result['project']);
$this->assertArrayHasKey('customer', $result['project']);
$this->assertArrayHasKey('id', $result['project']['customer']);
$this->assertArrayHasKey('name', $result['project']['customer']);
$this->assertArrayHasKey('visible', $result['project']['customer']);
}
} }

View File

@@ -49,21 +49,21 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(7, \count($result)); $this->assertEquals(7, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertStructure($user, false); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
public function testGetCollectionWithQuery() public function testGetCollectionWithQuery()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 2, 'orderBy' => 'email', 'order' => 'DESC']); $this->assertAccessIsGranted($client, '/api/users', 'GET', ['visible' => 2, 'orderBy' => 'email', 'order' => 'DESC', 'term' => 'chris']);
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertStructure($user, false); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
@@ -77,7 +77,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(8, \count($result)); $this->assertEquals(8, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertStructure($user, false); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
@@ -88,7 +88,7 @@ class UserControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('UserEntity', $result);
self::assertEquals('1', $result['id']); self::assertEquals('1', $result['id']);
self::assertEquals('CFO', $result['title']); self::assertEquals('CFO', $result['title']);
self::assertEquals('Clara Haynes', $result['alias']); self::assertEquals('Clara Haynes', $result['alias']);
@@ -101,7 +101,7 @@ class UserControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('UserEntity', $result);
self::assertEquals('6', $result['id']); self::assertEquals('6', $result['id']);
self::assertEquals('Super Administrator', $result['title']); self::assertEquals('Super Administrator', $result['title']);
self::assertEquals('', $result['alias']); self::assertEquals('', $result['alias']);
@@ -125,7 +125,7 @@ class UserControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('UserEntity', $result);
} }
public function testPostAction() public function testPostAction()
@@ -150,7 +150,7 @@ class UserControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('UserEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertEquals('foo', $result['username']); self::assertEquals('foo', $result['username']);
self::assertEquals('test123', $result['avatar']); self::assertEquals('test123', $result['avatar']);
@@ -161,6 +161,50 @@ class UserControllerTest extends APIControllerBaseTest
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN'], $result['roles']); self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN'], $result['roles']);
} }
public function testPostActionWithShortPassword()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'username' => 'foo',
'email' => 'foo@example.com',
'avatar' => 'test123',
'title' => 'asdfghjkl',
'plainPassword' => '1234567',
'enabled' => true,
'language' => 'ru',
'timezone' => 'Europe/Paris',
'roles' => [
'ROLE_TEAMLEAD',
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['plainPassword']);
}
public function testPostActionWithValidationErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'username' => '',
'email' => '',
'plainPassword' => '123456',
'language' => 'xx',
'timezone' => 'XXX/YYY',
'roles' => [
'ABC',
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['username', 'email', 'plainPassword', 'language', 'timezone', 'roles']);
}
public function testPostActionWithInvalidUser() public function testPostActionWithInvalidUser()
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -216,7 +260,7 @@ class UserControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertStructure($result); self::assertApiResponseTypeStructure('UserEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertEquals('foo', $result['username']); self::assertEquals('foo', $result['username']);
self::assertEquals('test321', $result['avatar']); self::assertEquals('test321', $result['avatar']);
@@ -227,21 +271,36 @@ class UserControllerTest extends APIControllerBaseTest
self::assertEquals(['ROLE_TEAMLEAD'], $result['roles']); self::assertEquals(['ROLE_TEAMLEAD'], $result['roles']);
} }
protected function assertStructure(array $result, $full = true) public function testPatchActionWithUnknownUser()
{ {
$expectedKeys = ['id', 'username', 'enabled', 'alias']; $this->assertEntityNotFoundForPatch(User::ROLE_SUPER_ADMIN, '/api/users/255', []);
}
if ($full) { public function testPatchActionWithInvalidUser()
$expectedKeys = array_merge( {
$expectedKeys, $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
['title', 'avatar', 'teams', 'roles', 'language', 'timezone'] $this->request($client, '/api/users/1', 'PATCH', [], json_encode(['avatar' => 'asdasd']));
); $this->assertApiResponseAccessDenied($client->getResponse(), 'Not allowed to edit user');
} }
$actual = array_keys($result); public function testPatchActionWithValidationErrors()
sort($actual); {
sort($expectedKeys); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'username' => '1', // not existing in form
'email' => '',
'plainPassword' => '123456', // not existing in form
'plainApiToken' => '123456', // not existing in form
'language' => 'xx',
'timezone' => 'XXX/YYY',
'roles' => [
'ABC',
],
];
$this->request($client, '/api/users/1', 'PATCH', [], json_encode($data));
$this->assertEquals($expectedKeys, $actual, 'User structure does not match'); $response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['email', 'language', 'timezone', 'roles'], true);
} }
} }

View File

@@ -100,4 +100,67 @@ class SecurityControllerTest extends ControllerBaseTest
$this->assertStringContainsString('<p>Congrats example, your account is now activated.</p>', $content); $this->assertStringContainsString('<p>Congrats example, your account is now activated.</p>', $content);
$this->assertStringContainsString('<a href="/en/homepage">', $content); $this->assertStringContainsString('<a href="/en/homepage">', $content);
} }
/**
* @dataProvider getValidationTestData
*/
public function testRegisterActionWithValidationProblems(array $formData, array $validationFields)
{
$client = self::createClient();
$this->assertHasValidationError($client, '/register/', 'form[name=fos_user_registration_form]', $formData, $validationFields);
}
public function getValidationTestData()
{
return [
[
// invalid fields: username, password_second, email
[
'fos_user_registration_form' => [
'username' => '',
'plainPassword' => ['first' => 'sdfsdf123'],
'email' => '',
]
],
[
'#fos_user_registration_form_username',
'#fos_user_registration_form_username',
'#fos_user_registration_form_plainPassword_first',
'#fos_user_registration_form_email',
'#fos_user_registration_form_email',
]
],
// invalid fields: username, password, email
[
[
'fos_user_registration_form' => [
'username' => 'x',
'plainPassword' => ['first' => 'sdfsdf123', 'second' => 'sdfxxxxxxx'],
'email' => 'ydfbvsdfgs',
]
],
[
'#fos_user_registration_form_username',
'#fos_user_registration_form_username',
'#fos_user_registration_form_plainPassword_first',
'#fos_user_registration_form_email',
'#fos_user_registration_form_email',
]
],
// invalid fields: password (too short)
[
[
'fos_user_registration_form' => [
'username' => 'test123',
'plainPassword' => ['first' => 'test123', 'second' => 'test123'],
'email' => 'ydfbvsdfgs@example.com',
]
],
[
'#fos_user_registration_form_plainPassword_first',
]
],
];
}
} }

View File

@@ -76,7 +76,7 @@ class UserControllerTest extends ControllerBaseTest
'user_create' => [ 'user_create' => [
'username' => $username, 'username' => $username,
'alias' => $username, 'alias' => $username,
'plainPassword' => ['first' => 'abcdef', 'second' => 'abcdef'], 'plainPassword' => ['first' => '12345678', 'second' => '12345678'],
'email' => 'foobar@example.com', 'email' => 'foobar@example.com',
'enabled' => 1, 'enabled' => 1,
] ]
@@ -198,7 +198,7 @@ class UserControllerTest extends ControllerBaseTest
[ [
'user_create' => [ 'user_create' => [
'username' => '', 'username' => '',
'plainPassword' => ['first' => 'sdfsdf'], 'plainPassword' => ['first' => 'sdfsdf123'],
'alias' => 'ycvyxcb', 'alias' => 'ycvyxcb',
'title' => '34rtwrtewrt', 'title' => '34rtwrtewrt',
'avatar' => 'asdfawer', 'avatar' => 'asdfawer',
@@ -216,7 +216,7 @@ class UserControllerTest extends ControllerBaseTest
[ [
'user_create' => [ 'user_create' => [
'username' => 'x', 'username' => 'x',
'plainPassword' => ['first' => 'sdfsdf', 'second' => 'sdfxxx'], 'plainPassword' => ['first' => 'sdfsdf123', 'second' => 'sdfxxxxxxx'],
'alias' => 'ycvyxcb', 'alias' => 'ycvyxcb',
'title' => '34rtwrtewrt', 'title' => '34rtwrtewrt',
'avatar' => 'asdfawer', 'avatar' => 'asdfawer',
@@ -229,6 +229,22 @@ class UserControllerTest extends ControllerBaseTest
'#user_create_email', '#user_create_email',
] ]
], ],
// invalid fields: password (too short)
[
[
'user_create' => [
'username' => 'test123',
'plainPassword' => ['first' => 'test123', 'second' => 'test123'],
'alias' => 'ycvyxcb',
'title' => '34rtwrtewrt',
'avatar' => 'asdfawer',
'email' => 'ydfbvsdfgs@example.com',
]
],
[
'#user_create_plainPassword_first',
]
],
]; ];
} }
} }

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Entity; namespace App\Tests\Entity;
use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/** /**
* Classes using this MUST extend \Symfony\Bundle\FrameworkBundle\Test\KernelTestCase * Classes using this MUST extend \Symfony\Bundle\FrameworkBundle\Test\KernelTestCase
@@ -20,12 +21,12 @@ trait EntityValidationTestTrait
* @param object $entity * @param object $entity
* @param array|string $fieldNames * @param array|string $fieldNames
*/ */
protected function assertHasViolationForField(object $entity, $fieldNames) protected function assertHasViolationForField(object $entity, $fieldNames, $groups = null)
{ {
self::bootKernel(); self::bootKernel();
/** @var ValidatorInterface $validator */
$validator = static::$kernel->getContainer()->get('validator'); $validator = static::$kernel->getContainer()->get('validator');
$violations = $validator->validate($entity, null, $groups);
$violations = $validator->validate($entity);
if (!\is_array($fieldNames)) { if (!\is_array($fieldNames)) {
$fieldNames = [$fieldNames]; $fieldNames = [$fieldNames];
@@ -55,12 +56,13 @@ trait EntityValidationTestTrait
$this->assertEquals($expected, $countViolations, sprintf('Expected %s violations, found %s in %s.', $expected, $actual, implode(', ', array_keys($violatedFields)))); $this->assertEquals($expected, $countViolations, sprintf('Expected %s violations, found %s in %s.', $expected, $actual, implode(', ', array_keys($violatedFields))));
} }
protected function assertHasNoViolations($entity) protected function assertHasNoViolations($entity, $groups = null)
{ {
self::bootKernel(); self::bootKernel();
/** @var ValidatorInterface $validator */
$validator = static::$kernel->getContainer()->get('validator'); $validator = static::$kernel->getContainer()->get('validator');
$violations = $validator->validate($entity); $violations = $validator->validate($entity, null, $groups);
$actual = $violations->count(); $actual = $violations->count();
$this->assertEquals(0, $actual, sprintf('Expected 0 violations, found %s.', $actual)); $this->assertEquals(0, $actual, sprintf('Expected 0 violations, found %s.', $actual));

View File

@@ -27,7 +27,6 @@ class UserValidationTest extends KernelTestCase
[null, null], [null, null],
['x', 'test@'], // too short username ['x', 'test@'], // too short username
[str_pad('#', 61, '-'), 'test@x.'], // too long username [str_pad('#', 61, '-'), 'test@x.'], // too long username
[str_pad('#', 61, '-'), 'test@x.', ['xxxxx']], // too short password and invalid role
]; ];
} }
@@ -48,14 +47,34 @@ class UserValidationTest extends KernelTestCase
$defaultFields[] = 'roles'; $defaultFields[] = 'roles';
} }
$this->assertHasViolationForField($user, $defaultFields); $this->assertHasViolationForField($user, $defaultFields, ['Profile']);
}
public function testInvalidRoles()
{
$user = new User();
$user->setUsername('foo');
$user->setEmail('foo@example.com');
$user->setRoles(['xxxxxx']);
$this->assertHasViolationForField($user, ['roles'], ['RolesUpdate']);
}
public function testValidRoles()
{
$user = new User();
$user->setUsername('foo');
$user->setEmail('foo@example.com');
$user->setRoles(['ROLE_TEAMLEAD']);
$this->assertHasNoViolations($user, ['RolesUpdate']);
} }
public function getValidTestData() public function getValidTestData()
{ {
return [ return [
[str_pad('#', 3, '-'), 'test@x.x'], // shortest possible username [str_pad('#', 8, '-'), 'test@x.x'], // shortest possible username
[str_pad('#', 60, '-'), 'test@x.x', ['ROLE_TEAMLEAD']], // longest possible password and valid role [str_pad('#', 60, '-'), 'test@x.x'], // longest possible username
]; ];
} }
@@ -71,6 +90,6 @@ class UserValidationTest extends KernelTestCase
$user->setRoles($roles); $user->setRoles($roles);
} }
$this->assertHasNoViolations($user); $this->assertHasNoViolations($user, ['Profile']);
} }
} }