added team permissions (#996)
This commit is contained in:
@@ -68,6 +68,7 @@ class CustomerController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
|
||||
@@ -70,6 +70,7 @@ class ProjectController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
|
||||
@@ -104,9 +104,7 @@ class TagController extends BaseApiController
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($tag);
|
||||
$entityManager->flush();
|
||||
$this->repository->deleteTag($tag);
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
|
||||
133
src/API/TeamController.php
Normal file
133
src/API/TeamController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Repository\TeamRepository;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Team")
|
||||
*/
|
||||
class TeamController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TeamRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all existing teams
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing teams",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/TeamCollection")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_team')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Team']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one team
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one team entity",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
/** @var Team $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a team
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=204,
|
||||
* description="Delete one team"
|
||||
* ),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Team ID to delete",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('delete_team')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$this->repository->deleteTeam($team);
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,9 @@ class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $this->find('user.language');
|
||||
}
|
||||
|
||||
public function getUserDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('user.currency');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
@@ -40,14 +41,19 @@ class ActivityController extends AbstractController
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
@@ -68,6 +74,7 @@ class ActivityController extends AbstractController
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -250,15 +257,20 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
if ($activity->getId() === null) {
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
} else {
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
|
||||
if ($activity->getId() !== null) {
|
||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||
if (null !== $activity->getProject()) {
|
||||
$currency = $activity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'action' => $url,
|
||||
'method' => 'POST',
|
||||
'currency' => $currency,
|
||||
'create_more' => true,
|
||||
'customer' => true,
|
||||
'include_budget' => $this->isGranted('budget', $activity)
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerTeamPermissionForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
@@ -71,14 +72,11 @@ class CustomerController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_customer')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -98,9 +96,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -117,12 +112,39 @@ class CustomerController extends AbstractController
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_customer_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', customer)")
|
||||
*/
|
||||
public function teamPermissions(Customer $customer, Request $request)
|
||||
{
|
||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/permissions.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_customer_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Customer $customer)
|
||||
{
|
||||
@@ -135,10 +157,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -148,10 +166,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -169,6 +183,7 @@ class CustomerController extends AbstractController
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setCustomerToIgnore($customer);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
@@ -229,11 +244,7 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(CustomerQuery $query)
|
||||
protected function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(CustomerToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
@@ -243,11 +254,7 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Customer $customer)
|
||||
private function createEditForm(Customer $customer): FormInterface
|
||||
{
|
||||
if ($customer->getId() === null) {
|
||||
$url = $this->generateUrl('admin_customer_create');
|
||||
|
||||
@@ -69,6 +69,7 @@ class ExportController extends AbstractController
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class InvoiceController extends AbstractController
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@ use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Voter\UserVoter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
@@ -175,6 +177,28 @@ class ProfileController extends AbstractController
|
||||
return $this->getProfileView($profile, 'roles', null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||
* @Security("is_granted('teams', profile)")
|
||||
*/
|
||||
public function teamsAction(User $profile, Request $request)
|
||||
{
|
||||
$form = $this->createTeamsForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'teams', null, null, null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||
* @Security("is_granted('preferences', profile)")
|
||||
@@ -236,24 +260,15 @@ class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $tab
|
||||
* @param Form|null $editForm
|
||||
* @param Form|null $pwdForm
|
||||
* @param Form|null $rolesForm
|
||||
* @param Form|null $apiTokenForm
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function getProfileView(
|
||||
User $user,
|
||||
string $tab,
|
||||
Form $editForm = null,
|
||||
Form $pwdForm = null,
|
||||
Form $rolesForm = null,
|
||||
Form $apiTokenForm = null
|
||||
) {
|
||||
FormInterface $editForm = null,
|
||||
FormInterface $pwdForm = null,
|
||||
FormInterface $rolesForm = null,
|
||||
FormInterface $apiTokenForm = null,
|
||||
FormInterface $teamsForm = null
|
||||
): Response {
|
||||
$forms = [];
|
||||
|
||||
if ($this->isGranted(UserVoter::EDIT, $user)) {
|
||||
@@ -268,6 +283,10 @@ class ProfileController extends AbstractController
|
||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||
$forms['api-token'] = $apiTokenForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::TEAMS, $user)) {
|
||||
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
|
||||
$forms['teams'] = $teamsForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||
$forms['roles'] = $rolesForm->createView();
|
||||
@@ -280,11 +299,7 @@ class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createPreferencesForm(User $user)
|
||||
private function createPreferencesForm(User $user): FormInterface
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($user);
|
||||
@@ -300,11 +315,7 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(User $user)
|
||||
private function createEditForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserEditType::class,
|
||||
@@ -317,11 +328,7 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createRolesForm(User $user)
|
||||
private function createRolesForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserRolesType::class,
|
||||
@@ -333,11 +340,19 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createPasswordForm(User $user)
|
||||
private function createTeamsForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserTeamsType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_teams', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createPasswordForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserPasswordType::class,
|
||||
@@ -350,11 +365,7 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createApiTokenForm(User $user)
|
||||
private function createApiTokenForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserApiTokenType::class,
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -40,14 +42,19 @@ class ProjectController extends AbstractController
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
@@ -60,14 +67,11 @@ class ProjectController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_project')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -85,14 +89,40 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_project_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', project)")
|
||||
*/
|
||||
public function teamPermissions(Project $project, Request $request)
|
||||
{
|
||||
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_project');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/permissions.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{customer}", name="admin_project_create_with_customer", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_project')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Customer|null $customer
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request, ?Customer $customer = null)
|
||||
{
|
||||
@@ -108,9 +138,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_project_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Project $project)
|
||||
{
|
||||
@@ -123,10 +150,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -136,10 +159,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -158,6 +177,7 @@ class ProjectController extends AbstractController
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->setCustomer($project->getCustomer());
|
||||
$query->setProjectToIgnore($project);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
@@ -225,11 +245,7 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ProjectQuery $query)
|
||||
protected function getToolbarForm(ProjectQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ProjectToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_project', [
|
||||
@@ -239,16 +255,12 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Project $project)
|
||||
private function createEditForm(Project $project): FormInterface
|
||||
{
|
||||
if ($project->getId() === null) {
|
||||
$url = $this->generateUrl('admin_project_create');
|
||||
$currency = Customer::DEFAULT_CURRENCY;
|
||||
} else {
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_project_create');
|
||||
|
||||
if ($project->getId() !== null) {
|
||||
$url = $this->generateUrl('admin_project_edit', ['id' => $project->getId()]);
|
||||
$currency = $project->getCustomer()->getCurrency();
|
||||
}
|
||||
|
||||
@@ -248,6 +248,10 @@ class SystemConfigurationController extends AbstractController
|
||||
->setName('defaults.user.theme')
|
||||
->setLabel('skin')
|
||||
->setType(SkinType::class),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.currency')
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_THEME)
|
||||
|
||||
185
src/Controller/TeamController.php
Normal file
185
src/Controller/TeamController.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\TeamCustomerForm;
|
||||
use App\Form\TeamEditForm;
|
||||
use App\Form\TeamProjectForm;
|
||||
use App\Form\Toolbar\TeamToolbarForm;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/teams")
|
||||
* @Security("is_granted('view_team')")
|
||||
*/
|
||||
class TeamController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(TeamRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_team", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_team_paginated", methods={"GET"})
|
||||
*
|
||||
* @param TeamRepository $repository
|
||||
* @param Request $request
|
||||
* @param int $page
|
||||
* @return Response
|
||||
*/
|
||||
public function listTeams(TeamRepository $repository, Request $request, $page)
|
||||
{
|
||||
$query = new TeamQuery();
|
||||
$query->setPage($page);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$teams = $repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('team/index.html.twig', [
|
||||
'teams' => $teams,
|
||||
'query' => $query,
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_team_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_team')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createTeam(Request $request)
|
||||
{
|
||||
return $this->renderEditScreen(new Team(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_team_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team)")
|
||||
*/
|
||||
public function editAction(Team $team, Request $request)
|
||||
{
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
private function renderEditScreen(Team $team, Request $request): Response
|
||||
{
|
||||
$customerForm = null;
|
||||
$projectForm = null;
|
||||
|
||||
if ($team->getId() === null) {
|
||||
$url = $this->generateUrl('admin_team_create');
|
||||
} else {
|
||||
$url = $this->generateUrl('admin_team_edit', ['id' => $team->getId()]);
|
||||
}
|
||||
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
'action' => $url,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($editFormValues = $request->get($editForm->getName())))) {
|
||||
$editForm->submit($editFormValues, true);
|
||||
|
||||
if ($editForm->isValid()) {
|
||||
try {
|
||||
// make sure that the teamlead is always part of the team, otherwise permission checks
|
||||
// and filtering might not work as expected!
|
||||
$team->addUser($team->getTeamLead());
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $team->getId()) {
|
||||
$customerForm = $this->createForm(TeamCustomerForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($customerFormValues = $request->get($customerForm->getName())))) {
|
||||
$customerForm->submit($customerFormValues, true);
|
||||
|
||||
if ($customerForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$projectForm = $this->createForm(TeamProjectForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($projectFormValues = $request->get($projectForm->getName())))) {
|
||||
$projectForm->submit($projectFormValues, true);
|
||||
|
||||
if ($projectForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('team/edit.html.twig', [
|
||||
'team' => $team,
|
||||
'form' => $editForm->createView(),
|
||||
'customerForm' => $customerForm ? $customerForm->createView() : null,
|
||||
'projectForm' => $projectForm ? $projectForm->createView() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getToolbarForm(TeamQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TeamToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_team', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -112,9 +112,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
$dirtyQuery = $query->isDirty();
|
||||
|
||||
if (!$this->includeUserInForms()) {
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$pager = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
@@ -253,9 +251,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
if (!$this->includeUserInForms()) {
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$entries = $this->getRepository()->getTimesheetsForQuery($query);
|
||||
|
||||
@@ -265,6 +261,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||
{
|
||||
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetAdminEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -76,6 +77,11 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setCurrentUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getCreateFormClassName(): string
|
||||
{
|
||||
return TimesheetAdminEditForm::class;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
@@ -439,7 +440,7 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->scalarNode('timezone')->defaultNull()->end()
|
||||
->scalarNode('country')->defaultValue('DE')->end()
|
||||
->scalarNode('currency')->defaultValue('EUR')->end()
|
||||
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('user')
|
||||
@@ -448,9 +449,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('timezone')->defaultNull()->end()
|
||||
->scalarNode('language')->defaultValue(User::DEFAULT_LANGUAGE)->end()
|
||||
->scalarNode('theme')->defaultNull()->end()
|
||||
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||
->end()
|
||||
->end()
|
||||
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -160,9 +160,26 @@ class Customer implements EntityWithMetaFields
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"remove", "persist"}, inversedBy="customers")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_customers_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="customer_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -399,6 +416,33 @@ class Customer implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addCustomer($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeCustomer($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -90,9 +90,26 @@ class Project implements EntityWithMetaFields
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"remove", "persist"}, inversedBy="projects")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_projects_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="project_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -223,6 +240,33 @@ class Project implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addProject($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeProject($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
210
src/Entity/Team.php
Normal file
210
src/Entity/Team.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_teams",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TeamRepository")
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
class Team
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=100)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $teamlead;
|
||||
/**
|
||||
* @var User[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="User", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $users;
|
||||
/**
|
||||
* @var Customer[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $customers;
|
||||
/**
|
||||
* @var Project[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $projects;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->users = new ArrayCollection();
|
||||
$this->customers = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): Team
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getTeamLead(): ?User
|
||||
{
|
||||
return $this->teamlead;
|
||||
}
|
||||
|
||||
public function isTeamlead(User $user): bool
|
||||
{
|
||||
return $this->teamlead === $user;
|
||||
}
|
||||
|
||||
public function setTeamLead(User $teamlead): Team
|
||||
{
|
||||
$this->teamlead = $teamlead;
|
||||
$this->addUser($teamlead);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasUser(User $user): bool
|
||||
{
|
||||
return $this->users->contains($user);
|
||||
}
|
||||
|
||||
public function addUser(User $user)
|
||||
{
|
||||
if ($this->users->contains($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->users->add($user);
|
||||
$user->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeUser(User $user)
|
||||
{
|
||||
if (!$this->users->contains($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->users->removeElement($user);
|
||||
$user->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<User>
|
||||
*/
|
||||
public function getUsers(): iterable
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer)
|
||||
{
|
||||
if ($this->customers->contains($customer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->customers->add($customer);
|
||||
$customer->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeCustomer(Customer $customer)
|
||||
{
|
||||
if (!$this->customers->contains($customer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->customers->removeElement($customer);
|
||||
$customer->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Customer>
|
||||
*/
|
||||
public function getCustomers(): iterable
|
||||
{
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
public function addProject(Project $project)
|
||||
{
|
||||
if ($this->projects->contains($project)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->projects->add($project);
|
||||
$project->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeProject(Project $project)
|
||||
{
|
||||
if (!$this->projects->contains($project)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->projects->removeElement($project);
|
||||
$project->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function getProjects(): iterable
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
}
|
||||
@@ -140,14 +140,14 @@ class Timesheet implements EntityWithMetaFields
|
||||
/**
|
||||
* @var Tag[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Tag", inversedBy="timesheets", cascade={"persist"})
|
||||
* @ORM\ManyToMany(targetEntity="Tag", inversedBy="timesheets", cascade={"remove", "persist"})
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_timesheet_tags",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id")
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
||||
@@ -95,6 +95,22 @@ class User extends BaseUser implements UserInterface
|
||||
*/
|
||||
private $preferences;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", inversedBy="users", cascade={"remove", "persist"})
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_users_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -103,6 +119,7 @@ class User extends BaseUser implements UserInterface
|
||||
parent::__construct();
|
||||
$this->registeredAt = new \DateTime();
|
||||
$this->preferences = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -274,6 +291,55 @@ class User extends BaseUser implements UserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team): User
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addUser($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeUser($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
public function isInTeam(Team $team): bool
|
||||
{
|
||||
return $this->teams->contains($team);
|
||||
}
|
||||
|
||||
public function isTeamleadOf(Team $team): bool
|
||||
{
|
||||
return $team->getTeamLead() === $this;
|
||||
}
|
||||
|
||||
public function isTeamlead(): bool
|
||||
{
|
||||
return $this->hasRole(static::ROLE_TEAMLEAD);
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->hasRole(static::ROLE_ADMIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,6 @@ class UserPreference
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
@@ -47,7 +46,6 @@ class UserPreference
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -55,28 +53,29 @@ class UserPreference
|
||||
* @Assert\Length(min=2, max=50)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
private $type;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = true;
|
||||
|
||||
private $enabled = true;
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
protected $constraints = [];
|
||||
private $constraints = [];
|
||||
/**
|
||||
* An array of options for the form element
|
||||
* @var array
|
||||
*/
|
||||
private $options = [];
|
||||
|
||||
/**
|
||||
* @return int
|
||||
@@ -237,4 +236,27 @@ class UserPreference
|
||||
{
|
||||
return $this->constraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of options for the FormType.
|
||||
*
|
||||
* @param array $options
|
||||
* @return UserPreference
|
||||
*/
|
||||
public function setOptions(array $options): UserPreference
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with options for the FormType.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,12 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_team')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $this->getIcon('team'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('plugins')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $this->getIcon('plugin'))
|
||||
|
||||
@@ -66,6 +66,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
return $this->formConfig->getUserDefaultTheme();
|
||||
}
|
||||
|
||||
private function getDefaultCurrency(): ?string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultCurrency();
|
||||
}
|
||||
|
||||
private function getDefaultLanguage(): string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultLanguage();
|
||||
@@ -88,9 +93,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
public function getDefaultPreferences(User $user)
|
||||
{
|
||||
$enableHourlyRate = false;
|
||||
$hourlyRateOptions = [];
|
||||
|
||||
if ($this->voter->isGranted('hourly-rate', $user)) {
|
||||
$enableHourlyRate = true;
|
||||
$hourlyRateOptions = ['currency' => $this->getDefaultCurrency()];
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -99,6 +106,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setValue(0)
|
||||
->setType(MoneyType::class)
|
||||
->setEnabled($enableHourlyRate)
|
||||
->setOptions($hourlyRateOptions)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
|
||||
(new UserPreference())
|
||||
@@ -165,6 +173,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setType($preference->getType())
|
||||
->setConstraints($preference->getConstraints())
|
||||
->setEnabled($preference->isEnabled())
|
||||
->setOptions($preference->getOptions())
|
||||
;
|
||||
} else {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
|
||||
@@ -67,8 +67,11 @@ class ActivityEditForm extends AbstractType
|
||||
if ($options['customer']) {
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'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,
|
||||
@@ -80,15 +83,18 @@ class ActivityEditForm extends AbstractType
|
||||
$builder
|
||||
->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
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 ($project) {
|
||||
function (FormEvent $event) use ($builder, $project) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||
return;
|
||||
@@ -96,8 +102,11 @@ class ActivityEditForm extends AbstractType
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $data['customer']));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) {
|
||||
$query = new ProjectFormTypeQuery($project, $data['customer']);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
51
src/Form/CustomerTeamPermissionForm.php
Normal file
51
src/Form/CustomerTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Form\Type\TeamType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Customer::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_customer_teams_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerTeamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
src/Form/Extension/UserExtension.php
Normal file
45
src/Form/Extension/UserExtension.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class UserExtension extends AbstractTypeExtension
|
||||
{
|
||||
/**
|
||||
* @var CurrentUser
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [FormType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefined(['user']);
|
||||
// null needs to be allowed, as there is no user for anonymoud forms (like "forgot password" and "registration")
|
||||
$resolver->setAllowedTypes('user', [User::class, 'null']);
|
||||
$resolver->setDefault('user', $this->user->getUser());
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,11 @@ class ProjectEditForm extends AbstractType
|
||||
'required' => false,
|
||||
])
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
51
src/Form/ProjectTeamPermissionForm.php
Normal file
51
src/Form/ProjectTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Form\Type\TeamType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Project::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_project_teams_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectTeamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
51
src/Form/TeamCustomerForm.php
Normal file
51
src/Form/TeamCustomerForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamCustomerForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('customers', CustomerType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_customer',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
61
src/Form/TeamEditForm.php
Normal file
61
src/Form/TeamEditForm.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\UserType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamEditForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
])
|
||||
->add('teamlead', UserType::class, [
|
||||
'label' => 'label.teamlead',
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
])
|
||||
->add('users', UserType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
src/Form/TeamProjectForm.php
Normal file
52
src/Form/TeamProjectForm.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamProjectForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('projects', ProjectType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'by_reference' => false,
|
||||
'attr' => ['size' => '20'],
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_project',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,11 @@ class TimesheetEditForm extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
@@ -189,8 +192,11 @@ class TimesheetEditForm extends AbstractType
|
||||
array_merge($projectOptions, [
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
])
|
||||
);
|
||||
@@ -198,7 +204,7 @@ class TimesheetEditForm extends AbstractType
|
||||
// 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 ($project, $customer, $isNew) {
|
||||
function (FormEvent $event) use ($builder, $project, $customer, $isNew) {
|
||||
$data = $event->getData();
|
||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||
@@ -207,7 +213,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer, $isNew) {
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
|
||||
// is there a better wa to prevent starting a record with a hidden project ?
|
||||
if ($isNew && !is_object($project)) {
|
||||
/** @var Project $project */
|
||||
@@ -221,8 +227,10 @@ class TimesheetEditForm extends AbstractType
|
||||
}
|
||||
}
|
||||
}
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -66,13 +66,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($builder) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'project_enabled' => true,
|
||||
'query_builder' => function (CustomerRepository $repo) use ($data) {
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $data) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setUser($builder->getOption('user'));
|
||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||
$query->setCustomer($data['customer']);
|
||||
}
|
||||
@@ -138,13 +139,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($builder) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $data) {
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||
$query->setCustomer($data['customer']);
|
||||
|
||||
37
src/Form/Toolbar/TeamToolbarForm.php
Normal file
37
src/Form/Toolbar/TeamToolbarForm.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => TeamQuery::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,22 @@ class CustomerType extends AbstractType
|
||||
'label' => 'label.customer',
|
||||
'class' => Customer::class,
|
||||
'choice_label' => 'name',
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery());
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
'project_enabled' => false,
|
||||
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (CustomerRepository $repo) use ($options) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
|
||||
$resolver->setDefault('api_data', function (Options $options) {
|
||||
if (true === $options['project_enabled']) {
|
||||
return [
|
||||
|
||||
@@ -62,13 +62,22 @@ class ProjectType extends AbstractType
|
||||
'group_by' => function (Project $project, $key, $index) {
|
||||
return $project->getCustomer()->getName();
|
||||
},
|
||||
'query_builder' => function (ProjectRepository $repo) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery());
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
'activity_enabled' => false,
|
||||
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (ProjectRepository $repo) use ($options) {
|
||||
$query = new ProjectFormTypeQuery();
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
|
||||
$resolver->setDefault('api_data', function (Options $options) {
|
||||
if (true === $options['activity_enabled']) {
|
||||
return [
|
||||
|
||||
44
src/Form/Type/TeamType.php
Normal file
44
src/Form/Type/TeamType.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Repository\TeamRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'class' => Team::class,
|
||||
'label' => 'label.team',
|
||||
'query_builder' => function (TeamRepository $repo) {
|
||||
return $repo->createQueryBuilder('t')->orderBy('t.name', 'ASC');
|
||||
},
|
||||
'choice_label' => function (Team $team) {
|
||||
return $team->getName();
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return EntityType::class;
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,23 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Custom form field type to edit a user preference.
|
||||
*/
|
||||
class UserPreferenceType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translate;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translate = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param array $options
|
||||
@@ -54,12 +65,22 @@ class UserPreferenceType extends AbstractType
|
||||
$type = HiddenType::class;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $type, [
|
||||
'label' => 'label.' . $preference->getName(),
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
'disabled' => !$preference->isEnabled(),
|
||||
]);
|
||||
$transId = 'label.' . $preference->getName();
|
||||
if ($this->translate->trans($transId) === $transId) {
|
||||
$transId = $preference->getName();
|
||||
}
|
||||
|
||||
$options = array_merge(
|
||||
[
|
||||
'label' => $transId,
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
'disabled' => !$preference->isEnabled(),
|
||||
],
|
||||
$preference->getOptions()
|
||||
);
|
||||
|
||||
$event->getForm()->add('value', $type, $options);
|
||||
}
|
||||
);
|
||||
$builder->add('name', HiddenType::class);
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -35,6 +38,15 @@ class UserType extends AbstractType
|
||||
return $user->getUsername();
|
||||
},
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (UserRepository $repo) use ($options) {
|
||||
$query = new UserFormTypeQuery();
|
||||
$query->setUser($options['user']);
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
49
src/Form/UserTeamsType.php
Normal file
49
src/Form/UserTeamsType.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\TeamType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to assign a User to teams.
|
||||
*/
|
||||
class UserTeamsType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'label' => 'label.team',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_teams',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,6 @@ class Version20190510205245 extends AbstractMigration
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$tags = $schema->getTable('kimai2_tags');
|
||||
$tags->dropIndex('UNIQ_27CAF54C5E237E06');
|
||||
|
||||
$timesheetTags = $schema->getTable('kimai2_timesheet_tags');
|
||||
$timesheetTags->dropIndex('IDX_E3284EFEABDD46BE');
|
||||
$timesheetTags->dropIndex('IDX_E3284EFEBAD26311');
|
||||
|
||||
$schema->dropTable('kimai2_timesheet_tags');
|
||||
$schema->dropTable('kimai2_tags');
|
||||
}
|
||||
|
||||
44
src/Migrations/Version20190729162655.php
Normal file
44
src/Migrations/Version20190729162655.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Creates user team tables.
|
||||
*
|
||||
* @version 1.2
|
||||
*/
|
||||
final class Version20190729162655 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Fixes foreign keys on tag table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$timesheetTags = $schema->getTable('kimai2_timesheet_tags');
|
||||
|
||||
if (!$timesheetTags->hasForeignKey('FK_732EECA9ABDD46BE')) {
|
||||
$timesheetTags->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_732EECA9ABDD46BE');
|
||||
}
|
||||
if (!$timesheetTags->hasForeignKey('FK_732EECA9BAD26311')) {
|
||||
$timesheetTags->addForeignKeyConstraint('kimai2_tags', ['tag_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_732EECA9BAD26311');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
}
|
||||
}
|
||||
68
src/Migrations/Version20190730123324.php
Normal file
68
src/Migrations/Version20190730123324.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Fixes foreign keys on tag table.
|
||||
*
|
||||
* @version 1.2
|
||||
*/
|
||||
final class Version20190730123324 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Creates user team and permission tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$teams = $schema->createTable('kimai2_teams');
|
||||
$teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]);
|
||||
$teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$teams->setPrimaryKey(['id']);
|
||||
$teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06');
|
||||
$teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7');
|
||||
|
||||
$userTeams = $schema->createTable('kimai2_users_teams');
|
||||
$userTeams->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395');
|
||||
$userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE');
|
||||
$userTeams->setPrimaryKey(['user_id', 'team_id']);
|
||||
|
||||
$customerTeams = $schema->createTable('kimai2_customers_teams');
|
||||
$customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3');
|
||||
$customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE');
|
||||
$customerTeams->setPrimaryKey(['customer_id', 'team_id']);
|
||||
|
||||
$projectTeams = $schema->createTable('kimai2_projects_teams');
|
||||
$projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C');
|
||||
$projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE');
|
||||
$projectTeams->setPrimaryKey(['project_id', 'team_id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_projects_teams');
|
||||
$schema->dropTable('kimai2_customers_teams');
|
||||
$schema->dropTable('kimai2_users_teams');
|
||||
$schema->dropTable('kimai2_teams');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ActivityStatistic;
|
||||
use App\Repository\Loader\ActivityLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
@@ -81,6 +82,47 @@ class ActivityRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all activities
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||
}
|
||||
|
||||
$qb->leftJoin('p.teams', 'teams')
|
||||
->leftJoin('c.teams', 'c_teams');
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere($qb->expr()->isNull('c_teams'));
|
||||
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$orProject = $qb->expr()->orX(
|
||||
$qb->expr()->isNull('teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||
);
|
||||
$qb->andWhere($orProject);
|
||||
|
||||
$orCustomer = $qb->expr()->orX(
|
||||
$qb->expr()->isNull('c_teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($orCustomer);
|
||||
|
||||
$qb->setParameter('teams', $teams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.1
|
||||
*/
|
||||
@@ -181,16 +223,11 @@ class ActivityRepository extends EntityRepository
|
||||
$qb
|
||||
->select('a')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder())
|
||||
;
|
||||
|
||||
if (!$query->isGlobalsOnly()) {
|
||||
$qb
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
}
|
||||
|
||||
$where = $qb->expr()->andX();
|
||||
|
||||
if (in_array($query->getVisibility(), [ActivityQuery::SHOW_VISIBLE, ActivityQuery::SHOW_HIDDEN])) {
|
||||
@@ -239,6 +276,8 @@ class ActivityRepository extends EntityRepository
|
||||
$qb->andWhere($where);
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Repository\Loader\CustomerLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
@@ -107,8 +108,41 @@ class CustomerRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all customers
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all customers
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||
}
|
||||
|
||||
$qb->leftJoin('c.teams', 'teams');
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$or = $qb->expr()->orX(
|
||||
$qb->expr()->isNull('teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($or);
|
||||
|
||||
$qb->setParameter('teams', $teams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.1
|
||||
* @deprecated since 1.1 - don't use this method, it ignores team permission checks
|
||||
*/
|
||||
public function builderForEntityType($customer)
|
||||
{
|
||||
@@ -145,6 +179,8 @@ class CustomerRepository extends EntityRepository
|
||||
$qb->setParameter('ignored', $query->getCustomerToIgnore());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -152,7 +188,7 @@ class CustomerRepository extends EntityRepository
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('c', 'meta')
|
||||
$qb->select('c')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.meta', 'meta')
|
||||
->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
|
||||
@@ -165,6 +201,8 @@ class CustomerRepository extends EntityRepository
|
||||
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,27 @@ final class ActivityIdLoader implements LoaderInterface
|
||||
->andWhere($qb->expr()->in('p.id', $projectIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'PARTIAL project.{id}', 'teams', 'teamlead')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'project')
|
||||
->leftJoin('project.teams', 'teams')
|
||||
->leftJoin('teams.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('a.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'PARTIAL project.{id}', 'PARTIAL customer.{id}', 'teams', 'teamlead')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'project')
|
||||
->leftJoin('project.customer', 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->leftJoin('teams.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('a.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,14 @@ final class CustomerIdLoader implements LoaderInterface
|
||||
->andWhere($qb->expr()->in('c.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL c.{id}', 'teams', 'teamlead')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.teams', 'teams')
|
||||
->leftJoin('teams.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('c.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +50,24 @@ final class ProjectIdLoader implements LoaderInterface
|
||||
->andWhere($qb->expr()->in('p.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL p.{id}', 'teams', 'teamlead')
|
||||
->from(Project::class, 'p')
|
||||
->leftJoin('p.teams', 'teams')
|
||||
->leftJoin('teams.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('p.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL p.{id}', 'PARTIAL customer.{id}', 'teams', 'teamlead')
|
||||
->from(Project::class, 'p')
|
||||
->leftJoin('p.customer', 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->leftJoin('teams.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('p.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
46
src/Repository/Loader/TeamIdLoader.php
Normal file
46
src/Repository/Loader/TeamIdLoader.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class TeamIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
*/
|
||||
public function loadResults(array $ids): void
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'users')
|
||||
->from(Team::class, 't')
|
||||
->leftJoin('t.users', 'users')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
38
src/Repository/Loader/TeamLoader.php
Normal file
38
src/Repository/Loader/TeamLoader.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class TeamLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var TeamIdLoader
|
||||
*/
|
||||
private $loader;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->loader = new TeamIdLoader($entityManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team[] $teams
|
||||
*/
|
||||
public function loadResults(array $teams): void
|
||||
{
|
||||
$ids = array_map(function (Team $team) {
|
||||
return $team->getId();
|
||||
}, $teams);
|
||||
|
||||
$this->loader->loadResults($ids);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace App\Repository;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ProjectStatistic;
|
||||
use App\Repository\Loader\ProjectLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
@@ -91,8 +92,49 @@ class ProjectRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all projects
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||
}
|
||||
|
||||
$qb->leftJoin('p.teams', 'teams')
|
||||
->leftJoin('c.teams', 'c_teams');
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere($qb->expr()->isNull('c_teams'));
|
||||
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$orProject = $qb->expr()->orX(
|
||||
$qb->expr()->isNull('teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||
);
|
||||
$qb->andWhere($orProject);
|
||||
|
||||
$orCustomer = $qb->expr()->orX(
|
||||
$qb->expr()->isNull('c_teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($orCustomer);
|
||||
|
||||
$qb->setParameter('teams', $teams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.1
|
||||
* @deprecated since 1.1 - don't use this method, it ignores team permission checks
|
||||
*/
|
||||
public function builderForEntityType($project, $customer)
|
||||
{
|
||||
@@ -114,7 +156,7 @@ class ProjectRepository extends EntityRepository
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select('p', 'c')
|
||||
->select('p')
|
||||
->from(Project::class, 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->addOrderBy('c.name', 'ASC')
|
||||
@@ -140,6 +182,8 @@ class ProjectRepository extends EntityRepository
|
||||
$qb->setParameter('ignored', $query->getProjectToIgnore());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -150,11 +194,11 @@ class ProjectRepository extends EntityRepository
|
||||
$qb
|
||||
->select('p')
|
||||
->from(Project::class, 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
|
||||
if (in_array($query->getVisibility(), [ProjectQuery::SHOW_VISIBLE, ProjectQuery::SHOW_HIDDEN])) {
|
||||
$qb
|
||||
->leftJoin('p.customer', 'c')
|
||||
->andWhere($qb->expr()->eq('p.visible', ':visible'))
|
||||
->andWhere($qb->expr()->eq('c.visible', ':customer_visible'))
|
||||
;
|
||||
@@ -173,6 +217,8 @@ class ProjectRepository extends EntityRepository
|
||||
->setParameter('customer', $query->getCustomer());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
$qb->orderBy('p.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
return $qb;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* Base class for advanced Repository queries.
|
||||
*/
|
||||
@@ -44,6 +47,45 @@ class BaseQuery
|
||||
* @var string
|
||||
*/
|
||||
private $resultType = self::RESULT_TYPE_PAGER;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var Team[]
|
||||
*/
|
||||
private $teams = [];
|
||||
|
||||
public function addTeam(Team $team): self
|
||||
{
|
||||
$this->teams[$team->getId()] = $team;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeams(): array
|
||||
{
|
||||
return array_values($this->teams);
|
||||
}
|
||||
|
||||
public function getCurrentUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return $this
|
||||
*/
|
||||
public function setCurrentUser(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* Can be used for advanced queries with the: CustomerRepository
|
||||
@@ -24,6 +26,14 @@ final class CustomerFormTypeQuery
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customerToIgnore;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var array<Team>
|
||||
*/
|
||||
private $teams = [];
|
||||
|
||||
/**
|
||||
* @param Customer|int|null $customer
|
||||
@@ -33,6 +43,33 @@ final class CustomerFormTypeQuery
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team): CustomerFormTypeQuery
|
||||
{
|
||||
$this->teams[$team->getId()] = $team;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeams(): array
|
||||
{
|
||||
return array_values($this->teams);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): CustomerFormTypeQuery
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer|int|null
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
|
||||
final class ProjectFormTypeQuery
|
||||
{
|
||||
@@ -26,6 +28,14 @@ final class ProjectFormTypeQuery
|
||||
* @var Project|null
|
||||
*/
|
||||
private $projectToIgnore;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var array<Team>
|
||||
*/
|
||||
private $teams = [];
|
||||
|
||||
/**
|
||||
* @param Project|int|null $project
|
||||
@@ -37,6 +47,33 @@ final class ProjectFormTypeQuery
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team): ProjectFormTypeQuery
|
||||
{
|
||||
$this->teams[$team->getId()] = $team;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeams(): array
|
||||
{
|
||||
return array_values($this->teams);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): ProjectFormTypeQuery
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer|int|null
|
||||
*/
|
||||
|
||||
@@ -11,4 +11,8 @@ namespace App\Repository\Query;
|
||||
|
||||
class TagQuery extends BaseQuery
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->setOrderBy('name');
|
||||
}
|
||||
}
|
||||
|
||||
18
src/Repository/Query/TeamQuery.php
Normal file
18
src/Repository/Query/TeamQuery.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
class TeamQuery extends BaseQuery
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->setOrderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
protected $timesheetUser;
|
||||
/**
|
||||
* @var Activity|null
|
||||
*/
|
||||
@@ -63,7 +63,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
return $this->timesheetUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
*/
|
||||
public function setUser($user = null)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->timesheetUser = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -256,7 +256,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->user !== null) {
|
||||
if ($this->timesheetUser !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
55
src/Repository/Query/UserFormTypeQuery.php
Normal file
55
src/Repository/Query/UserFormTypeQuery.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* Can be used for pre-filling form types with the: UserRepository
|
||||
*/
|
||||
final class UserFormTypeQuery
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var array<Team>
|
||||
*/
|
||||
private $teams = [];
|
||||
|
||||
public function addTeam(Team $team): UserFormTypeQuery
|
||||
{
|
||||
$this->teams[$team->getId()] = $team;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
public function getTeams(): array
|
||||
{
|
||||
return array_values($this->teams);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): UserFormTypeQuery
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,26 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
class TagRepository extends EntityRepository
|
||||
{
|
||||
use RepositoryTrait;
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function deleteTag(Tag $tag)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($tag);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ids of the given tagNames separated by comma
|
||||
@@ -68,7 +82,7 @@ class TagRepository extends EntityRepository
|
||||
* - amount
|
||||
*
|
||||
* @param TagQuery $query
|
||||
* @return array|\Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
public function getTagCount(TagQuery $query)
|
||||
{
|
||||
@@ -79,9 +93,12 @@ class TagRepository extends EntityRepository
|
||||
->leftJoin('tag.timesheets', 'timesheets')
|
||||
->addGroupBy('tag.id')
|
||||
->addGroupBy('tag.name')
|
||||
->orderBy('tag.name')
|
||||
;
|
||||
->orderBy('tag.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($qb->getQuery(), false));
|
||||
$paginator->setMaxPerPage($query->getPageSize());
|
||||
$paginator->setCurrentPage($query->getPage());
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
|
||||
99
src/Repository/TeamRepository.php
Normal file
99
src/Repository/TeamRepository.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Loader\TeamLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
class TeamRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @param Team $team
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveTeam(Team $team)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($team);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team $team
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function deleteTeam(Team $team)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($team);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(TeamQuery $query): Pagerfanta
|
||||
{
|
||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||
$paginator->setMaxPerPage($query->getPageSize());
|
||||
$paginator->setCurrentPage($query->getPage());
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
protected function getPaginatorForQuery(TeamQuery $query): PaginatorInterface
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->countDistinct('t.id'))
|
||||
;
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return new LoaderPaginator(new TeamLoader($qb->getEntityManager()), $qb, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeamQuery $query
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTeamsForQuery(TeamQuery $query): iterable
|
||||
{
|
||||
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||
$paginator = $this->getPaginatorForQuery($query);
|
||||
|
||||
return $paginator->getAll();
|
||||
}
|
||||
|
||||
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select('t')
|
||||
->from(Team::class, 't')
|
||||
;
|
||||
|
||||
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
@@ -456,6 +456,36 @@ class TimesheetRepository extends EntityRepository
|
||||
return $counter;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all timesheet records
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb
|
||||
->leftJoin('p.customer', 'c')
|
||||
->leftJoin('p.teams', 'teams')
|
||||
->leftJoin('c.teams', 'c_teams');
|
||||
|
||||
$orTeam = $qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':teams', 'p.teams'),
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($orTeam);
|
||||
|
||||
$qb->setParameter('teams', $teams);
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(TimesheetQuery $query): Pagerfanta
|
||||
{
|
||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||
@@ -500,11 +530,43 @@ class TimesheetRepository extends EntityRepository
|
||||
$qb
|
||||
->select('t')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.project', 'p')
|
||||
;
|
||||
|
||||
$user = [];
|
||||
if (null !== $query->getUser()) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $query->getUser());
|
||||
$user[] = $query->getUser();
|
||||
}
|
||||
|
||||
if (null === $query->getUser() && null !== $query->getCurrentUser()) {
|
||||
$currentUser = $query->getCurrentUser();
|
||||
|
||||
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
||||
foreach ($currentUser->getTeams() as $team) {
|
||||
if ($currentUser->isTeamleadOf($team)) {
|
||||
$query->addTeam($team);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($query->getTeams())) {
|
||||
foreach ($query->getTeams() as $team) {
|
||||
$user = array_merge($user, $team->getUsers()->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
$user = array_map(function ($user) {
|
||||
if ($user instanceof User) {
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}, $user);
|
||||
$user = array_unique($user);
|
||||
|
||||
if (!empty($user)) {
|
||||
$qb->andWhere($qb->expr()->in('t.user', $user));
|
||||
}
|
||||
|
||||
if (null !== $query->getBegin()) {
|
||||
@@ -539,7 +601,6 @@ class TimesheetRepository extends EntityRepository
|
||||
$qb->andWhere('t.project = :project')
|
||||
->setParameter('project', $query->getProject());
|
||||
} elseif (null !== $query->getCustomer()) {
|
||||
$qb->join('t.project', 'p');
|
||||
$qb->andWhere('p.customer = :customer')
|
||||
->setParameter('customer', $query->getCustomer());
|
||||
}
|
||||
@@ -551,6 +612,8 @@ class TimesheetRepository extends EntityRepository
|
||||
->setParameter('tags', $query->getTags());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
|
||||
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
return $qb;
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
|
||||
class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
@@ -26,12 +28,28 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch the currently logged-in user.
|
||||
*
|
||||
* @param int $id
|
||||
* @return null|User
|
||||
*/
|
||||
public function getUserById($id): ?User
|
||||
{
|
||||
return $this->find($id);
|
||||
try {
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p', 't', 'tu', 'tl')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->leftJoin('u.teams', 't')
|
||||
->leftJoin('t.users', 'tu')
|
||||
->leftJoin('t.teamlead', 'tl')
|
||||
->where('u.id = :id')
|
||||
->setParameter('id', $id)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,12 +121,49 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p')
|
||||
->select('u', 'p', 't', 'tu', 'tl')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->leftJoin('u.teams', 't')
|
||||
->leftJoin('t.users', 'tu')
|
||||
->leftJoin('t.teamlead', 'tl')
|
||||
->where('u.username = :username')
|
||||
->orWhere('u.email = :username')
|
||||
->setParameter('username', $username)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
}
|
||||
|
||||
public function getQueryBuilderForFormType(UserFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('u.enabled', ':enabled'));
|
||||
$qb->setParameter('enabled', true, \PDO::PARAM_BOOL);
|
||||
|
||||
$qb->orderBy('u.username', 'ASC');
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all user
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->leftJoin('u.teams', 'teams')
|
||||
->leftJoin('teams.users', 'users')
|
||||
->andWhere('teams.teamlead = :id')
|
||||
->setParameter('id', $user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,20 @@ use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class CurrentUser
|
||||
final class CurrentUser
|
||||
{
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
private $storage;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @param TokenStorageInterface $storage
|
||||
@@ -43,6 +47,11 @@ class CurrentUser
|
||||
return null;
|
||||
}
|
||||
|
||||
// some inline caching to prevent multiple DB lookups
|
||||
if (null !== $this->user) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
@@ -50,6 +59,8 @@ class CurrentUser
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->repository->getUserById($user->getId());
|
||||
$this->user = $this->repository->getUserById($user->getId());
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ final class IconExtension extends AbstractExtension
|
||||
'stop-small' => 'far fa-stop-circle',
|
||||
'timesheet' => 'fas fa-clock',
|
||||
'trash' => 'far fa-trash-alt',
|
||||
'user' => 'fas fa-users',
|
||||
'team' => 'fas fa-users',
|
||||
'user' => 'fas fa-user-friends',
|
||||
'visibility' => 'far fa-eye',
|
||||
'settings' => 'fas fa-cog',
|
||||
'export' => 'fas fa-file-export',
|
||||
@@ -64,6 +65,7 @@ final class IconExtension extends AbstractExtension
|
||||
'profile' => 'fas fa-user-edit',
|
||||
'warning' => 'fas fa-exclamation-triangle',
|
||||
'permissions' => 'fas fa-user-lock',
|
||||
'unlocked' => 'fas fa-unlock-alt',
|
||||
'back' => 'fas fa-long-arrow-alt-left',
|
||||
'tag' => 'fas fa-tags',
|
||||
'avatar' => 'fas fa-user',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
@@ -65,6 +66,44 @@ class ActivityVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_activity');
|
||||
if ($this->hasRolePermission($user, $attribute . '_activity')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$project = $subject->getProject();
|
||||
if (null === $project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_activity');
|
||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_activity');
|
||||
|
||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($project->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($project->getCustomer()->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
@@ -22,6 +23,7 @@ class CustomerVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const BUDGET = 'budget';
|
||||
public const DELETE = 'delete';
|
||||
public const PERMISSIONS = 'permissions';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Customer)
|
||||
@@ -31,6 +33,7 @@ class CustomerVoter extends AbstractVoter
|
||||
self::EDIT,
|
||||
self::BUDGET,
|
||||
self::DELETE,
|
||||
self::PERMISSIONS,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -65,6 +68,28 @@ class CustomerVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_customer');
|
||||
if ($this->hasRolePermission($user, $attribute . '_customer')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_customer');
|
||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_customer');
|
||||
|
||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($subject->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
@@ -22,6 +23,7 @@ class ProjectVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const BUDGET = 'budget';
|
||||
public const DELETE = 'delete';
|
||||
public const PERMISSIONS = 'permissions';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Project)
|
||||
@@ -31,6 +33,7 @@ class ProjectVoter extends AbstractVoter
|
||||
self::EDIT,
|
||||
self::BUDGET,
|
||||
self::DELETE,
|
||||
self::PERMISSIONS,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -65,6 +68,39 @@ class ProjectVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_project');
|
||||
if ($this->hasRolePermission($user, $attribute . '_project')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_project');
|
||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_project');
|
||||
|
||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($subject->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($subject->getCustomer()->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
65
src/Voter/TeamVoter.php
Normal file
65
src/Voter/TeamVoter.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
class TeamVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Team)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param Team $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!($subject instanceof Team)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param Team $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_team');
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class UserVoter extends AbstractVoter
|
||||
public const DELETE = 'delete';
|
||||
public const PASSWORD = 'password';
|
||||
public const ROLES = 'roles';
|
||||
public const TEAMS = 'teams';
|
||||
public const PREFERENCES = 'preferences';
|
||||
public const API_TOKEN = 'api-token';
|
||||
public const HOURLY_RATE = 'hourly-rate';
|
||||
@@ -30,6 +31,7 @@ class UserVoter extends AbstractVoter
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::ROLES,
|
||||
self::TEAMS,
|
||||
self::PASSWORD,
|
||||
self::DELETE,
|
||||
self::PREFERENCES,
|
||||
@@ -93,6 +95,7 @@ class UserVoter extends AbstractVoter
|
||||
case self::PASSWORD:
|
||||
case self::API_TOKEN:
|
||||
case self::ROLES:
|
||||
case self::TEAMS:
|
||||
case self::HOURLY_RATE:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user