diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index 4cab00bf..881318b6 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -36,6 +36,7 @@ nelmio_api_doc: - { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] } - { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team, Team_Entity] } - { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] } + - { alias: Team, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] } - { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] } - { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] } - { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] } diff --git a/src/API/TeamController.php b/src/API/TeamController.php index f02b07fd..1b9bb9e9 100644 --- a/src/API/TeamController.php +++ b/src/API/TeamController.php @@ -93,7 +93,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Returns one team entity", - * @SWG\Schema(ref="#/definitions/TeamEntity"), + * @SWG\Schema(ref="#/definitions/Team"), * ) * * @Security("is_granted('view_team')") @@ -160,7 +160,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Returns the new created team", - * @SWG\Schema(ref="#/definitions/TeamEntity"), + * @SWG\Schema(ref="#/definitions/Team"), * ) * ) * @SWG\Parameter( @@ -205,7 +205,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Returns the updated team", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -277,7 +277,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Adds a new user to a team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -336,7 +336,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Removes a user from the team. The teamlead cannot be removed.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -399,7 +399,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Adds a new customer to a team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -458,7 +458,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Removes a customer from the team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -517,7 +517,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Adds a new project to a team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -576,7 +576,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Removes a project from the team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -635,7 +635,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Adds a new activity to a team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( @@ -694,7 +694,7 @@ final class TeamController extends BaseApiController * @SWG\Response( * response=200, * description="Removes a activity from the team.", - * @SWG\Schema(ref="#/definitions/TeamEntity") + * @SWG\Schema(ref="#/definitions/Team") * ) * ) * @SWG\Parameter( diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index 6f7e35b6..0925ad60 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -421,7 +421,6 @@ final class ActivityController extends AbstractController 'action' => $url, 'method' => 'POST', 'currency' => $currency, - 'customer' => true, 'include_budget' => $this->isGranted('budget', $activity) ]); } diff --git a/src/Entity/Team.php b/src/Entity/Team.php index a8994190..3586ab72 100644 --- a/src/Entity/Team.php +++ b/src/Entity/Team.php @@ -349,7 +349,7 @@ class Team * @Serializer\VirtualProperty * @Serializer\SerializedName("users"), * @Serializer\Groups({"Team_Entity"}) - * @SWG\Property(ref="#/definitions/User") + * @SWG\Property(type="array", @SWG\Items(ref="#/definitions/User")) * * @return User[] */ diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php index 0c4517ce..67c3d742 100644 --- a/src/Entity/TeamMember.php +++ b/src/Entity/TeamMember.php @@ -51,7 +51,7 @@ class TeamMember * * @Serializer\Expose() * @Serializer\Groups({"Default", "Entity", "User_Entity"}) - * @SWG\Property(ref="#/definitions/TeamEntity") + * @SWG\Property(ref="#/definitions/Team") * * @ORM\ManyToOne(targetEntity="App\Entity\Team", inversedBy="members") * @ORM\JoinColumn(onDelete="CASCADE", nullable=false) diff --git a/src/Entity/User.php b/src/Entity/User.php index 51b4134f..719873b9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -173,6 +173,8 @@ class User implements UserInterface, EquatableInterface, \Serializable */ private $preferences; /** + * List of all team memberships. + * * @var TeamMember[]|ArrayCollection * * @Serializer\Expose() @@ -273,6 +275,8 @@ class User implements UserInterface, EquatableInterface, \Serializable */ private $passwordRequestedAt; /** + * List of all role names + * * @Serializer\Expose() * @Serializer\Groups({"User_Entity"}) * @Serializer\Type("array") @@ -625,10 +629,12 @@ class User implements UserInterface, EquatableInterface, \Serializable } /** + * List of all teams, this user is part of + * * @Serializer\VirtualProperty * @Serializer\SerializedName("teams"), * @Serializer\Groups({"User_Entity"}) - * @SWG\Property(ref="#/definitions/TeamEntity") + * @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team")) * * @return Team[] */ diff --git a/src/Form/ActivityEditForm.php b/src/Form/ActivityEditForm.php index 31cb2034..5d97cd06 100644 --- a/src/Form/ActivityEditForm.php +++ b/src/Form/ActivityEditForm.php @@ -11,18 +11,13 @@ namespace App\Form; use App\Entity\Activity; use App\Entity\Customer; -use App\Form\Type\CustomerType; use App\Form\Type\ProjectType; -use App\Repository\CustomerRepository; use App\Repository\ProjectRepository; -use App\Repository\Query\CustomerFormTypeQuery; use App\Repository\Query\ProjectFormTypeQuery; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; class ActivityEditForm extends AbstractType @@ -37,6 +32,7 @@ class ActivityEditForm extends AbstractType $project = null; $customer = null; $new = true; + $isGlobal = false; if (isset($options['data'])) { /** @var Activity $entry */ @@ -46,6 +42,8 @@ class ActivityEditForm extends AbstractType $project = $entry->getProject(); $customer = $project->getCustomer(); $options['currency'] = $customer->getCurrency(); + } else { + $isGlobal = null === $entry->getProject(); } $new = $entry->getId() === null; @@ -64,23 +62,7 @@ class ActivityEditForm extends AbstractType ]) ; - if ($new) { - if ($options['customer']) { - $builder - ->add('customer', CustomerType::class, [ - 'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) { - $query = new CustomerFormTypeQuery($customer); - $query->setUser($builder->getOption('user')); - - return $repo->getQueryBuilderForFormType($query); - }, - 'data' => $customer ? $customer : null, - 'required' => false, - 'mapped' => false, - 'project_enabled' => true, - ]); - } - + if ($new || !$isGlobal) { $builder ->add('project', ProjectType::class, [ 'required' => false, @@ -92,28 +74,6 @@ class ActivityEditForm extends AbstractType return $repo->getQueryBuilderForFormType($query); }, ]); - - // replaces the project select after submission, to make sure only projects for the selected customer are displayed - $builder->addEventListener( - FormEvents::PRE_SUBMIT, - function (FormEvent $event) use ($builder, $project) { - $data = $event->getData(); - if (!isset($data['customer']) || empty($data['customer'])) { - return; - } - - $event->getForm()->add('project', ProjectType::class, [ - 'group_by' => null, - 'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) { - $query = new ProjectFormTypeQuery($project, $data['customer']); - $query->setUser($builder->getOption('user')); - $query->setIgnoreDate(true); - - return $repo->getQueryBuilderForFormType($query); - }, - ]); - } - ); } $this->addCommonFields($builder, $options); @@ -129,6 +89,7 @@ class ActivityEditForm extends AbstractType 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_token_id' => 'admin_activity_edit', + // @deprecated not supported since 1.15, which removed the customer select completely 'customer' => false, 'currency' => Customer::DEFAULT_CURRENCY, 'include_budget' => false, diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php index 4e0f492d..fe936b08 100644 --- a/tests/API/ActivityControllerTest.php +++ b/tests/API/ActivityControllerTest.php @@ -91,6 +91,10 @@ class ActivityControllerTest extends APIControllerBaseTest $this->assertUrlIsSecured('/api/activities'); } + /** + * @return array + * @throws \Exception + */ protected function loadActivityTestData(): array { $em = $this->getEntityManager(); @@ -111,8 +115,8 @@ class ActivityControllerTest extends APIControllerBaseTest $activity = (new Activity())->setName('second one')->setComment('2'); $em->persist($activity); - $activity = (new Activity())->setName('third one')->setComment('3')->setProject($project); - $em->persist($activity); + $activity1 = (new Activity())->setName('third one')->setComment('3')->setProject($project); + $em->persist($activity1); $activity = (new Activity())->setName('fourth one')->setComment('4')->setProject($project2)->setVisible(false); $em->persist($activity); @@ -131,7 +135,7 @@ class ActivityControllerTest extends APIControllerBaseTest $em->flush(); - return [$project, $project2]; + return [$project, $project2, $activity1]; } /** @@ -304,6 +308,29 @@ class ActivityControllerTest extends APIControllerBaseTest $this->assertNotEmpty($result['id']); } + public function testPatchActionWithNonGlobalActivity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $imports = $this->loadActivityTestData(); + + $data = [ + 'name' => 'foo', + 'comment' => '', + 'visible' => true, + 'project' => $imports[1]->getId(), + 'budget' => '999', + 'timeBudget' => '7200', + ]; + $this->request($client, '/api/activities/' . $imports[2]->getId(), 'PATCH', [], json_encode($data)); + $this->assertTrue($client->getResponse()->isSuccessful()); + + $result = json_decode($client->getResponse()->getContent(), true); + $this->assertIsArray($result); + self::assertApiResponseTypeStructure('ActivityEntity', $result); + $this->assertNotEmpty($result['id']); + $this->assertEquals($imports[1]->getId(), $result['project']); + } + public function testPatchActionWithInvalidUser() { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); diff --git a/tests/Form/ActivityEditFormTest.php b/tests/Form/ActivityEditFormTest.php new file mode 100644 index 00000000..11ac0181 --- /dev/null +++ b/tests/Form/ActivityEditFormTest.php @@ -0,0 +1,84 @@ +factory->createBuilder(ActivityEditForm::class, $model); + + $attr = $form->getFormConfig()->getOption('attr'); + self::assertArrayHasKey('data-form-event', $attr); + self::assertEquals('kimai.activityUpdate', $attr['data-form-event']); + + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('project')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + } + + public function testWithGlobalNewActivityAndOptions() + { + $model = new Activity(); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_budget' => true + ]); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + } + + public function testWithGlobalExistingActivityAndOptions() + { + $model = $this->createMock(Activity::class); + $model->expects($this->once())->method('getId')->willReturn(1); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_budget' => true + ]); + self::assertFalse($form->has('project')); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + } + + public function testWithNonGlobalExistingActivityAndOptions() + { + $project = new Project(); + $customer = new Customer(); + $project->setCustomer($customer); + $model = $this->createMock(Activity::class); + + $model->expects($this->any())->method('getId')->willReturn(1); + $model->expects($this->any())->method('getProject')->willReturn($project); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_budget' => true + ]); + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('project')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + self::assertTrue($form->has('project')); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + } +}