added team permissions for activities (#1872)

This commit is contained in:
Kevin Papst
2020-08-08 18:50:04 +02:00
committed by GitHub
parent dc162bf385
commit 0914ebf737
84 changed files with 4300 additions and 3279 deletions

View File

@@ -13,6 +13,7 @@ namespace App\API;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\User;
use App\Event\ActivityMetaDefinitionEvent;
use App\Form\API\ActivityApiEditForm;
use App\Form\API\ActivityRateApiForm;
@@ -96,7 +97,11 @@ class ActivityController extends BaseApiController
*/
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
{
/** @var User $user */
$user = $this->getUser();
$query = new ActivityQuery();
$query->setCurrentUser($user);
if (null !== ($order = $paramFetcher->get('order'))) {
$query->setOrder($order);

View File

@@ -11,11 +11,13 @@ declare(strict_types=1);
namespace App\API;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User;
use App\Form\API\TeamApiEditForm;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TeamRepository;
@@ -39,8 +41,8 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
*/
final class TeamController extends BaseApiController
{
public const GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity'];
public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Team'];
/**
@@ -266,7 +268,7 @@ final class TeamController extends BaseApiController
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new user to a team. The user must not be deactivated.",
* description="Adds a new user to a team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
@@ -388,7 +390,7 @@ final class TeamController extends BaseApiController
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new customer to a team. The customer must not be invisible.",
* description="Adds a new customer to a team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
@@ -427,10 +429,6 @@ final class TeamController extends BaseApiController
throw new NotFoundException('Customer not found');
}
if (!$customer->isVisible()) {
throw new BadRequestHttpException('Cannot grant access to an invisible customer');
}
if ($team->hasCustomer($customer)) {
throw new BadRequestHttpException('Team has already access to customer');
}
@@ -510,7 +508,7 @@ final class TeamController extends BaseApiController
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new project to a team. The project must not be invisible.",
* description="Adds a new project to a team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
@@ -549,10 +547,6 @@ final class TeamController extends BaseApiController
throw new NotFoundException('Project not found');
}
if (!$project->isVisible()) {
throw new BadRequestHttpException('Cannot grant access to an invisible project');
}
if ($team->hasProject($project)) {
throw new BadRequestHttpException('Team has already access to project');
}
@@ -625,4 +619,122 @@ final class TeamController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Grant the team access to an activity
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new activity to a team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team that is granted access",
* required=true,
* )
* @SWG\Parameter(
* name="activityId",
* in="path",
* type="integer",
* description="The activity to grant acecess to (Activity ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postActivityAction(int $id, int $activityId, ActivityRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Activity|null $activity */
$activity = $repository->find($activityId);
if (null === $activity) {
throw new NotFoundException('Activity not found');
}
if ($team->hasActivity($activity)) {
throw new BadRequestHttpException('Team has already access to activity');
}
$team->addActivity($activity);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view);
}
/**
* Revokes access for an activity from a team
*
* @SWG\Delete(
* @SWG\Response(
* response=200,
* description="Removes a activity from the team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team whose permission will be revoked",
* required=true,
* )
* @SWG\Parameter(
* name="activityId",
* in="path",
* type="integer",
* description="The activity to remove (Activity ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteActivityAction(int $id, int $activityId, ActivityRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Activity|null $activity */
$activity = $repository->find($activityId);
if (null === $activity) {
throw new NotFoundException('Activity not found');
}
if (!$team->hasActivity($activity)) {
throw new BadRequestHttpException('Activity is not assigned to the team');
}
$team->removeActivity($activity);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view);
}
}

View File

@@ -14,6 +14,7 @@ use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\Team;
use App\Event\ActivityMetaDefinitionEvent;
use App\Event\ActivityMetaDisplayEvent;
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
@@ -21,12 +22,15 @@ use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
use App\Export\Spreadsheet\Writer\XlsxWriter;
use App\Form\ActivityEditForm;
use App\Form\ActivityRateForm;
use App\Form\ActivityTeamPermissionForm;
use App\Form\Toolbar\ActivityToolbarForm;
use App\Form\Type\ActivityType;
use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\ActivityQuery;
use App\Repository\TeamRepository;
use Exception;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -40,7 +44,7 @@ use Symfony\Component\Routing\Annotation\Route;
* Controller used to manage activities in the admin part of the site.
*
* @Route(path="/admin/activity")
* @Security("is_granted('view_activity')")
* @Security("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')")
*/
final class ActivityController extends AbstractController
{
@@ -67,11 +71,6 @@ final class ActivityController extends AbstractController
/**
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
* @Security("is_granted('view_activity')")
*
* @param int $page
* @param Request $request
* @return Response
*/
public function indexAction($page, Request $request)
{
@@ -114,15 +113,20 @@ final class ActivityController extends AbstractController
* @Route(path="/{id}/details", name="activity_details", methods={"GET", "POST"})
* @Security("is_granted('view', activity)")
*/
public function detailsAction(Activity $activity, ActivityRateRepository $rateRepository)
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository)
{
$event = new ActivityMetaDefinitionEvent($activity);
$this->dispatcher->dispatch($event);
$stats = null;
$rates = [];
$teams = null;
$defaultTeam = null;
if ($this->isGranted('edit', $activity)) {
if ($this->isGranted('create_team')) {
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
}
$rates = $rateRepository->getRatesForActivity($activity);
}
@@ -130,10 +134,16 @@ final class ActivityController extends AbstractController
$stats = $this->repository->getActivityStatistics($activity);
}
if ($this->isGranted('permissions', $activity) || $this->isGranted('details', $activity) || $this->isGranted('view_team')) {
$teams = $activity->getTeams();
}
return $this->render('activity/details.html.twig', [
'activity' => $activity,
'stats' => $stats,
'rates' => $rates
'rates' => $rates,
'team' => $defaultTeam,
'teams' => $teams,
]);
}
@@ -159,7 +169,7 @@ final class ActivityController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
} catch (\Exception $ex) {
} catch (Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}
@@ -174,10 +184,6 @@ final class ActivityController extends AbstractController
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
* @Route(path="/create/{project}", name="admin_activity_create_with_project", methods={"GET", "POST"})
* @Security("is_granted('create_activity')")
*
* @param Request $request
* @param Project|null $project
* @return RedirectResponse|Response
*/
public function createAction(Request $request, ?Project $project = null)
{
@@ -189,13 +195,66 @@ final class ActivityController extends AbstractController
return $this->renderActivityForm($activity, $request);
}
/**
* @Route(path="/{id}/permissions", name="admin_activity_permissions", methods={"GET", "POST"})
* @Security("is_granted('permissions', activity)")
*/
public function teamPermissionsAction(Activity $activity, Request $request)
{
$form = $this->createForm(ActivityTeamPermissionForm::class, $activity, [
'action' => $this->generateUrl('admin_activity_permissions', ['id' => $activity->getId()]),
'method' => 'POST',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->repository->saveActivity($activity);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('admin_activity');
} catch (Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->render('activity/permissions.html.twig', [
'activity' => $activity,
'form' => $form->createView()
]);
}
/**
* @Route(path="/{id}/create_team", name="activity_team_create", methods={"GET"})
* @Security("is_granted('create_team') and is_granted('permissions', activity)")
*/
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository)
{
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
if (null !== $defaultTeam) {
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
}
$defaultTeam = new Team();
$defaultTeam->setName($activity->getName());
$defaultTeam->setTeamLead($this->getUser());
$defaultTeam->addActivity($activity);
try {
$teamRepository->saveTeam($defaultTeam);
} catch (Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
}
/**
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', activity)")
*
* @param Activity $activity
* @param Request $request
* @return RedirectResponse|Response
*/
public function editAction(Activity $activity, Request $request)
{
@@ -205,10 +264,6 @@ final class ActivityController extends AbstractController
/**
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', activity)")
*
* @param Activity $activity
* @param Request $request
* @return RedirectResponse|Response
*/
public function deleteAction(Activity $activity, Request $request)
{
@@ -242,7 +297,7 @@ final class ActivityController extends AbstractController
try {
$this->repository->deleteActivity($activity, $deleteForm->get('activity')->getData());
$this->flashSuccess('action.delete.success');
} catch (\Exception $ex) {
} catch (Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
@@ -314,7 +369,7 @@ final class ActivityController extends AbstractController
} else {
return $this->redirectToRoute('admin_activity');
}
} catch (\Exception $ex) {
} catch (Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}

View File

@@ -96,7 +96,9 @@ final class PermissionController extends AbstractController
new PermissionSection('Project (Admin)', '_project'),
new PermissionSection('Project (Team member)', '_team_project'),
new PermissionSection('Project (Teamlead)', '_teamlead_project'),
new PermissionSection('Activity', '_activity'),
new PermissionSection('Activity (Admin)', '_activity'),
new PermissionSection('Activity (Team member)', '_team_activity'),
new PermissionSection('Activity (Teamlead)', '_teamlead_activity'),
new PermissionSection('Timesheet', '_timesheet'),
new PermissionSection('Timesheet (other)', '_other_timesheet'),
new PermissionSection('Timesheet (own)', '_own_timesheet'),

View File

@@ -14,6 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -42,7 +43,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* options={
* @Serializer\SerializedName("project"),
* @Serializer\Type(name="integer"),
* @Serializer\Groups({"Default"})
* @Serializer\Groups({"Activity", "Team", "Not_Expanded"})
* }
* )
*
@@ -70,6 +71,10 @@ class Activity implements EntityWithMetaFields
/**
* @var Project|null
*
* @Serializer\Expose()
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(ref="#/definitions/Project")
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
@@ -164,10 +169,34 @@ class Activity implements EntityWithMetaFields
* @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"})
*/
private $meta;
/**
* Teams
*
* If no team is assigned, everyone can access the activity
*
* @var Team[]|ArrayCollection
*
* @Serializer\Expose()
* @Serializer\Groups({"Activity"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
*
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="activities")
* @ORM\JoinTable(
* name="kimai2_activities_teams",
* joinColumns={
* @ORM\JoinColumn(name="activity_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
@@ -300,6 +329,33 @@ class Activity implements EntityWithMetaFields
return $this;
}
public function addTeam(Team $team)
{
if ($this->teams->contains($team)) {
return $this;
}
$this->teams->add($team);
$team->addActivity($this);
}
public function removeTeam(Team $team)
{
if (!$this->teams->contains($team)) {
return;
}
$this->teams->removeElement($team);
$team->removeActivity($this);
}
/**
* @return Collection<Team>
*/
public function getTeams(): Collection
{
return $this->teams;
}
/**
* @return string
*/

View File

@@ -76,7 +76,7 @@ class Project implements EntityWithMetaFields
*
* @Serializer\Expose()
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Customer"))
* @SWG\Property(ref="#/definitions/Customer")
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)

View File

@@ -114,12 +114,27 @@ class Team
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
*/
private $projects;
/**
* Activities
*
* All activities assigned to the team
*
* @var Activity[]|ArrayCollection
*
* @Serializer\Expose()
* @Serializer\Groups({"Team_Entity", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Activity"))
*
* @ORM\ManyToMany(targetEntity="Activity", mappedBy="teams", fetch="EXTRA_LAZY")
*/
private $activities;
public function __construct()
{
$this->users = new ArrayCollection();
$this->customers = new ArrayCollection();
$this->projects = new ArrayCollection();
$this->activities = new ArrayCollection();
}
public function getId(): ?int
@@ -256,6 +271,39 @@ class Team
return $this->projects;
}
public function hasActivity(Activity $activity): bool
{
return $this->activities->contains($activity);
}
public function addActivity(Activity $activity)
{
if ($this->activities->contains($activity)) {
return;
}
$this->activities->add($activity);
$activity->addTeam($this);
}
public function removeActivity(Activity $activity)
{
if (!$this->activities->contains($activity)) {
return;
}
$this->activities->removeElement($activity);
$activity->removeTeam($this);
}
/**
* @return Collection<Activity>
*/
public function getActivities(): iterable
{
return $this->activities;
}
/**
* @return string
*/

View File

@@ -165,8 +165,8 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
* @var Activity
*
* @Serializer\Expose()
* @Serializer\Groups({"Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/ActivityExpanded"))
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(ref="#/definitions/ActivityExpanded")
*
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
@@ -180,7 +180,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
*
* @Serializer\Expose()
* @Serializer\Groups({"Subresource", "Expanded"})
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/ProjectExpanded"))
* @SWG\Property(ref="#/definitions/ProjectExpanded")
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)

View File

@@ -111,7 +111,7 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu->addChild($projects);
}
if ($auth->isGranted('view_activity')) {
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $this->getIcon('activity'));
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
$menu->addChild($activities);

View 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\Activity;
use App\Form\Type\TeamType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityTeamPermissionForm 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' => Activity::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_activity_teams_edit',
'attr' => [
'data-form-event' => 'kimai.activityTeamUpdate'
],
]);
}
}

View File

@@ -15,7 +15,7 @@ use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Fixes foreign keys on tag table.
* Creates user team and permission tables.
*
* @version 1.2
*/

View File

@@ -0,0 +1,43 @@
<?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 the activity teams table.
*
* @version 1.10
*/
final class Version20200725213424 extends AbstractMigration
{
public function getDescription(): string
{
return 'Creates the activity teams table';
}
public function up(Schema $schema): void
{
$activityTeams = $schema->createTable('kimai2_activities_teams');
$activityTeams->addColumn('activity_id', 'integer', ['length' => 11, 'notnull' => true]);
$activityTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$activityTeams->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA81C06096');
$activityTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_986998DA296CD8AE');
$activityTeams->setPrimaryKey(['activity_id', 'team_id']);
}
public function down(Schema $schema): void
{
$schema->dropTable('kimai2_activities_teams');
}
}

View File

@@ -130,18 +130,26 @@ class ActivityRepository extends EntityRepository
$teams = array_merge($teams, $user->getTeams()->toArray());
}
$qb->leftJoin('p.teams', 'teams')
$qb->leftJoin('a.teams', 'teams')
->leftJoin('p.teams', 'p_teams')
->leftJoin('c.teams', 'c_teams');
if (empty($teams)) {
$qb->andWhere($qb->expr()->isNull('c_teams'));
$qb->andWhere($qb->expr()->isNull('teams'));
$qb->andWhere($qb->expr()->isNull('p_teams'));
$qb->andWhere($qb->expr()->isNull('c_teams'));
return;
}
$orProject = $qb->expr()->orX(
$orActivity = $qb->expr()->orX(
$qb->expr()->isNull('teams'),
$qb->expr()->isMemberOf(':teams', 'a.teams')
);
$qb->andWhere($orActivity);
$orProject = $qb->expr()->orX(
$qb->expr()->isNull('p_teams'),
$qb->expr()->isMemberOf(':teams', 'p.teams')
);
$qb->andWhere($orProject);
@@ -253,6 +261,7 @@ class ActivityRepository extends EntityRepository
$qb
->select('a')
->distinct()
->from(Activity::class, 'a')
->leftJoin('a.project', 'p')
->leftJoin('p.customer', 'c')
@@ -320,7 +329,7 @@ class ActivityRepository extends EntityRepository
$qb->andWhere($where);
}
$this->addPermissionCriteria($qb, $query->getCurrentUser());
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
if ($query->hasSearchTerm()) {
$searchAnd = $qb->expr()->andX();
@@ -353,13 +362,6 @@ class ActivityRepository extends EntityRepository
}
}
// this will make sure, that we do not accidentally create results with multiple rows
// => which would result in a wrong LIMIT / pagination results
// $qb->addGroupBy('a.id');
// the second group by is needed due to SQL standard (even though logically not really required for this query)
// $qb->addGroupBy($orderBy);
return $qb;
}

View File

@@ -214,6 +214,7 @@ class CustomerRepository extends EntityRepository
$qb
->select('c')
->distinct()
->from(Customer::class, 'c')
;
@@ -268,13 +269,6 @@ class CustomerRepository extends EntityRepository
}
}
// this will make sure, that we do not accidentally create results with multiple rows
// => which would result in a wrong LIMIT / pagination results
// $qb->addGroupBy('c.id');
// the second group by is needed due to SQL standard (even though logically not really required for this query)
// $qb->addGroupBy($orderBy);
return $qb;
}

View File

@@ -137,6 +137,7 @@ class InvoiceRepository extends EntityRepository
$qb
->select('i')
->distinct()
->from(Invoice::class, 'i')
;
@@ -151,13 +152,6 @@ class InvoiceRepository extends EntityRepository
$this->addPermissionCriteria($qb, $query->getCurrentUser());
// this will make sure, that we do not accidentally create results with multiple rows
// => which would result in a wrong LIMIT / pagination results
// $qb->addGroupBy('i.id');
// the second group by is needed due to SQL standard (even though logically not really required for this query)
// $qb->addGroupBy($orderBy);
return $qb;
}

View File

@@ -93,6 +93,15 @@ final class ActivityIdLoader implements LoaderInterface
->andWhere($qb->expr()->in('a.id', $ids))
->getQuery()
->execute();
$qb = $em->createQueryBuilder();
$qb->select('PARTIAL a.{id}', 'teams', 'teamlead')
->from(Activity::class, 'a')
->leftJoin('a.teams', 'teams')
->leftJoin('teams.teamlead', 'teamlead')
->andWhere($qb->expr()->in('a.id', $ids))
->getQuery()
->execute();
}
}
}

View File

@@ -255,6 +255,7 @@ class ProjectRepository extends EntityRepository
$qb
->select('p')
->distinct()
->from(Project::class, 'p')
->leftJoin('p.customer', 'c')
;
@@ -366,13 +367,6 @@ class ProjectRepository extends EntityRepository
}
}
// this will make sure, that we do not accidentally create results with multiple rows
// => which would result in a wrong LIMIT / pagination results
// $qb->addGroupBy('p.id');
// the second group by is needed due to SQL standard (even though logically not really required for this query)
// $qb->addGroupBy($orderBy);
return $qb;
}

View File

@@ -19,19 +19,15 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
*/
class ActivityVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const BUDGET = 'budget';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: Activity)
* support rules based on the given activity
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::BUDGET,
self::DELETE,
'view',
'edit',
'budget',
'delete',
'permissions',
];
/**
@@ -70,8 +66,8 @@ class ActivityVoter extends AbstractVoter
return true;
}
// new and global activities have no project
if (null === ($project = $subject->getProject())) {
// those cannot be assigned to teams
if (\in_array($attribute, ['create', 'delete'])) {
return false;
}
@@ -82,6 +78,22 @@ class ActivityVoter extends AbstractVoter
return false;
}
/** @var Team $team */
foreach ($subject->getTeams() as $team) {
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
return true;
}
if ($hasTeamPermission && $user->isInTeam($team)) {
return true;
}
}
// new and global activities have no project
if (null === ($project = $subject->getProject())) {
return false;
}
/** @var Team $team */
foreach ($project->getTeams() as $team) {
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {