added team permissions for activities (#1872)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
51
src/Form/ActivityTeamPermissionForm.php
Normal file
51
src/Form/ActivityTeamPermissionForm.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\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'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
43
src/Migrations/Version20200725213424.php
Normal file
43
src/Migrations/Version20200725213424.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user