added project start and end date (#1303)

* added sortable js library
* activity in invoice is optional
* added javascript widget for paginated boxes
* fix activity dropdown for globals only
* added timesheet service to reduce code duplication
* use repository to query for teams in dropdowns
* added project validator
* validate project start and end against timesheet
* include begin and end in dynamic form requests for projects
* added timezone and language option to import flag, improve timesheet import speed
* deactivate cross-timezone filter
* add virtual fields to field order list
* composer update
* added param to ignore dates
* position loader icon fixed - fixes #1330
* permission problem when creating a new project - fixes #1340
* remove dev dependencies webserver and thanks bundle
* stop information leak (begin and end date) in duration mode - fixes #1307
* unify timesheet edit dialog for user and admins
* fix security issue, own rates exposed to unauthorized users in multi-update dialog
This commit is contained in:
Kevin Papst
2020-01-05 02:49:01 +01:00
committed by GitHub
parent 50383673ca
commit d6798eed1e
121 changed files with 2465 additions and 1439 deletions

View File

@@ -16,6 +16,7 @@ use App\Event\ProjectMetaDefinitionEvent;
use App\Form\API\ProjectApiEditForm;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectQuery;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\SearchTerm;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\RouteResource;
@@ -29,6 +30,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Validator\Constraints;
/**
* @RouteResource("Project")
@@ -49,16 +51,21 @@ class ProjectController extends BaseApiController
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* @var UserDateTimeFactory
*/
private $dateTime;
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher)
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->dispatcher = $dispatcher;
$this->dateTime = $dateTime;
}
/**
* Returns a collection of projects
* Returns a collection of projects.
*
* @SWG\Response(
* response=200,
@@ -69,7 +76,10 @@ class ProjectController extends BaseApiController
* )
* )
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects")
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects (1=visible, 2=hidden, 3=both)")
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects. Allowed values: 1=visible, 2=hidden, 3=both (default; 1)")
* @Rest\QueryParam(name="start", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Only projects that started before this date will be included. Allowed format: HTML5 (default: now, if end is also empty)")
* @Rest\QueryParam(name="end", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Only projects that ended after this date will be included. Allowed format: HTML5 (default: now, if start is also empty)")
* @Rest\QueryParam(name="ignoreDates", requirements="1", strict=true, nullable=true, description="If set, start and end are completely ignored. Allowed values: 1 (default: off)")
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
* @Rest\QueryParam(name="orderBy", requirements="id|name|customer", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, customer (default: name)")
* @Rest\QueryParam(name="term", requirements="[a-zA-Z0-9 \-,:]+", strict=true, nullable=true, description="Free search term")
@@ -98,6 +108,27 @@ class ProjectController extends BaseApiController
$query->setVisibility($visible);
}
$ignoreDates = false;
if (null !== $paramFetcher->get('ignoreDates')) {
$ignoreDates = intval($paramFetcher->get('ignoreDates')) === 1;
}
if (!$ignoreDates) {
if (null !== ($begin = $paramFetcher->get('start')) && !empty($begin)) {
$query->setProjectStart($this->dateTime->createDateTime($begin));
}
if (null !== ($end = $paramFetcher->get('end')) && !empty($end)) {
$query->setProjectEnd($this->dateTime->createDateTime($end));
}
if (empty($begin) && empty($end)) {
$now = $this->dateTime->createDateTime();
$query->setProjectStart($now);
$query->setProjectEnd($now);
}
}
if (!empty($term = $paramFetcher->get('term'))) {
$query->setSearchTerm(new SearchTerm($term));
}

View File

@@ -20,6 +20,7 @@ use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository;
use App\Repository\TimesheetRepository;
use App\Timesheet\RoundingService;
use App\Timesheet\TimesheetService;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Timesheet\TrackingModeService;
use App\Timesheet\UserDateTimeFactory;
@@ -81,6 +82,10 @@ class TimesheetController extends BaseApiController
* @var RoundingService
*/
private $roundingService;
/**
* @var TimesheetService
*/
private $service;
public function __construct(
ViewHandlerInterface $viewHandler,
@@ -90,7 +95,8 @@ class TimesheetController extends BaseApiController
TagRepository $tagRepository,
TrackingModeService $trackingModeService,
EventDispatcherInterface $dispatcher,
RoundingService $roundingService
RoundingService $roundingService,
TimesheetService $service
) {
$this->viewHandler = $viewHandler;
$this->repository = $repository;
@@ -100,6 +106,7 @@ class TimesheetController extends BaseApiController
$this->trackingModeService = $trackingModeService;
$this->dispatcher = $dispatcher;
$this->roundingService = $roundingService;
$this->service = $service;
}
protected function getTrackingMode(): TrackingModeInterface
@@ -295,14 +302,9 @@ class TimesheetController extends BaseApiController
*/
public function postAction(Request $request): Response
{
$timesheet = new Timesheet();
$timesheet->setUser($this->getUser());
$event = new TimesheetMetaDefinitionEvent($timesheet);
$this->dispatcher->dispatch($event);
$timesheet = $this->service->createNewTimesheet($this->getUser(), $request);
$mode = $this->getTrackingMode();
$mode->create($timesheet, $request);
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
'include_rate' => $this->isGranted('edit_rate', $timesheet),
@@ -316,18 +318,16 @@ class TimesheetController extends BaseApiController
$form->submit($request->request->all(), false);
if ($form->isValid()) {
if (null === $timesheet->getEnd()) {
if (!$this->isGranted('start', $timesheet)) {
throw new AccessDeniedHttpException('You are not allowed to start this timesheet record');
try {
$this->service->saveNewTimesheet($timesheet);
} catch (\Exception $ex) {
if ($ex->getMessage() === 'timesheet.start.exceeded_limit') {
throw new BadRequestHttpException('Too many active timesheets');
} else {
throw $ex;
}
$this->repository->stopActiveEntries(
$timesheet->getUser(),
$this->configuration->getActiveEntriesHardLimit()
);
}
$this->repository->save($timesheet);
$view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
@@ -559,7 +559,7 @@ class TimesheetController extends BaseApiController
throw new AccessDeniedHttpException('You are not allowed to stop this timesheet');
}
$this->repository->stopRecording($timesheet);
$this->service->stopTimesheet($timesheet);
$view = new View($timesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
@@ -600,13 +600,10 @@ class TimesheetController extends BaseApiController
throw new AccessDeniedHttpException('You are not allowed to re-start this timesheet');
}
/** @var User $user */
$user = $this->getUser();
$copyTimesheet = $this->service->createNewTimesheet($this->getUser());
$copyTimesheet = new Timesheet();
$copyTimesheet
->setBegin($this->dateTime->createDateTime())
->setUser($user)
->setActivity($timesheet->getActivity())
->setProject($timesheet->getProject())
;
@@ -642,12 +639,7 @@ class TimesheetController extends BaseApiController
throw new BadRequestHttpException($errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage());
}
$this->repository->stopActiveEntries(
$user,
$this->configuration->getActiveEntriesHardLimit()
);
$this->repository->save($copyTimesheet);
$this->service->saveNewTimesheet($copyTimesheet);
$view = new View($copyTimesheet, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);