diff --git a/lib/Alchemy/Phrasea/Collection/Reference/ArrayCacheCollectionReferenceRepository.php b/lib/Alchemy/Phrasea/Collection/Reference/ArrayCacheCollectionReferenceRepository.php index 7351351e93..64c80ae955 100644 --- a/lib/Alchemy/Phrasea/Collection/Reference/ArrayCacheCollectionReferenceRepository.php +++ b/lib/Alchemy/Phrasea/Collection/Reference/ArrayCacheCollectionReferenceRepository.php @@ -72,6 +72,25 @@ class ArrayCacheCollectionReferenceRepository implements CollectionReferenceRepo return null; } + /** + * @param array $baseIds + * @return CollectionReference[] + */ + public function findMany(array $baseIds) + { + $references = $this->findAll(); + + $requested = []; + + foreach ($baseIds as $baseId) { + if (isset($references[$baseId])) { + $requested[] = $references[$baseId]; + } + } + + return $requested; + } + /** * @param int $databoxId * @param int $collectionId @@ -90,6 +109,10 @@ class ArrayCacheCollectionReferenceRepository implements CollectionReferenceRepo return null; } + /** + * @param array|null $baseIdsSubset + * @return CollectionReference[] + */ public function findHavingOrderMaster(array $baseIdsSubset = null) { return $this->repository->findHavingOrderMaster($baseIdsSubset); diff --git a/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceCollection.php b/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceCollection.php new file mode 100644 index 0000000000..45390e63a5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceCollection.php @@ -0,0 +1,58 @@ +references = $references instanceof \Traversable ? iterator_to_array($references) : $references; + } + + /** + * Returns an array of array with actual index as leaf value. + * + * @return array> + */ + public function groupByDataboxIdAndCollectionId() + { + $groups = []; + + foreach ($this->references as $index => $reference) { + $databoxId = $reference->getDataboxId(); + $group = isset($groups[$databoxId]) ? $groups[$databoxId] : []; + + $group[$reference->getCollectionId()] = $index; + $groups[$databoxId] = $group; + } + + return $groups; + } + + /** + * @return \ArrayIterator|CollectionReference[] + */ + public function getIterator() + { + return new \ArrayIterator($this->references); + } +} diff --git a/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceRepository.php b/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceRepository.php index fdbf7289d1..7740f8ddc9 100644 --- a/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceRepository.php +++ b/lib/Alchemy/Phrasea/Collection/Reference/CollectionReferenceRepository.php @@ -29,6 +29,12 @@ interface CollectionReferenceRepository */ public function find($baseId); + /** + * @param int[] $baseIds + * @return CollectionReference[] + */ + public function findMany(array $baseIds); + /** * @param int $databoxId * @param int $collectionId diff --git a/lib/Alchemy/Phrasea/Collection/Reference/DbalCollectionReferenceRepository.php b/lib/Alchemy/Phrasea/Collection/Reference/DbalCollectionReferenceRepository.php index a67f8cad3d..00c8101259 100644 --- a/lib/Alchemy/Phrasea/Collection/Reference/DbalCollectionReferenceRepository.php +++ b/lib/Alchemy/Phrasea/Collection/Reference/DbalCollectionReferenceRepository.php @@ -95,6 +95,25 @@ WHERE base_id = :baseId'; return null; } + /** + * @param array $basesId + * @return CollectionReference[] + */ + public function findMany(array $basesId) + { + if (empty($basesId)) { + return []; + } + + $rows = $this->connection->fetchAll( + self::$selectQuery . ' WHERE base_id IN (:baseIds)', + ['baseIds' => $basesId], + ['baseIds' => Connection::PARAM_INT_ARRAY] + ); + + return $this->createManyReferences($rows); + } + /** * @param int $databoxId * @param int $collectionId diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php b/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php index 7fa1d9bc84..b6e7c8ac8e 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php @@ -12,10 +12,11 @@ namespace Alchemy\Phrasea\ControllerProvider\Api; use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Controller\Api\BasketController; use Alchemy\Phrasea\Controller\Api\LazaretController; -use Alchemy\Phrasea\Controller\Api\OrderController; use Alchemy\Phrasea\Controller\Api\SearchController; +use Alchemy\Phrasea\Controller\LazyLocator; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; use Alchemy\Phrasea\Core\Event\Listener\OAuthListener; +use Alchemy\Phrasea\Order\Controller\ApiOrderController; use Silex\Application; use Silex\Controller; use Silex\ControllerProviderInterface; @@ -51,8 +52,9 @@ class V2 implements ControllerProviderInterface, ServiceProviderInterface $app['controller.api.v2.orders'] = $app->share( function (PhraseaApplication $app) { - return (new OrderController($app)) + return (new ApiOrderController($app)) ->setDispatcher($app['dispatcher']) + ->setEntityManagerLocator(new LazyLocator($app, 'orm.em')) ->setJsonBodyHelper($app['json.body_helper']); } ); @@ -102,8 +104,17 @@ class V2 implements ControllerProviderInterface, ServiceProviderInterface $controllers->get('/orders/', 'controller.api.v2.orders:indexAction') ->bind('api_v2_orders_index'); $controllers->get('/orders/{orderId}', 'controller.api.v2.orders:showAction') + ->assert('orderId', '\d+') ->bind('api_v2_orders_show'); + $controllers->post('/orders/{orderId}/accept', 'controller.api.v2.orders:acceptElementsAction') + ->assert('orderId', '\d+') + ->bind('api_v2_orders_accept'); + + $controllers->post('/orders/{orderId}/deny', 'controller.api.v2.orders:denyElementsAction') + ->assert('orderId', '\d+') + ->bind('api_v2_orders_deny'); + return $controllers; } diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Order.php b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Order.php index febdc98c08..983bacdbad 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Order.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Order.php @@ -13,8 +13,10 @@ namespace Alchemy\Phrasea\ControllerProvider\Prod; use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Controller\LazyLocator; -use Alchemy\Phrasea\Controller\Prod\OrderController; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; +use Alchemy\Phrasea\Order\Controller\ProdOrderController; +use Alchemy\Phrasea\Order\OrderBasketProvider; +use Alchemy\Phrasea\Order\OrderValidator; use Silex\Application; use Silex\ControllerProviderInterface; use Silex\ServiceProviderInterface; @@ -25,8 +27,19 @@ class Order implements ControllerProviderInterface, ServiceProviderInterface public function register(Application $app) { + $app['provider.order_basket'] = $app->share(function (PhraseaApplication $app) { + return new OrderBasketProvider($app['orm.em'], $app['translator']); + }); + + $app['validator.order'] = $app->share(function (PhraseaApplication $app) { + $orderValidator = new OrderValidator($app['phraseanet.appbox'], $app['repo.collection-references']); + $orderValidator->setAclProvider($app['acl']); + + return $orderValidator; + }); + $app['controller.prod.order'] = $app->share(function (PhraseaApplication $app) { - return (new OrderController($app)) + return (new ProdOrderController($app)) ->setDispatcher($app['dispatcher']) ->setEntityManagerLocator(new LazyLocator($app, 'orm.em')) ->setUserQueryFactory(new LazyLocator($app, 'phraseanet.user-query')) diff --git a/lib/Alchemy/Phrasea/Model/Entities/Order.php b/lib/Alchemy/Phrasea/Model/Entities/Order.php index 5fe32c9d95..4363343438 100644 --- a/lib/Alchemy/Phrasea/Model/Entities/Order.php +++ b/lib/Alchemy/Phrasea/Model/Entities/Order.php @@ -1,9 +1,8 @@ todo; } + /** + * @param int $count + */ + public function decrementTodo($count) + { + $this->todo -= $count; + } + /** * Returns the total number of elements. * @@ -250,8 +258,16 @@ class Order */ public function setBasket(Basket $basket = null) { + if ($this->basket) { + $this->basket->setOrder(null); + } + $this->basket = $basket; + if ($basket) { + $basket->setOrder($this); + } + return $this; } diff --git a/lib/Alchemy/Phrasea/Model/Entities/OrderElement.php b/lib/Alchemy/Phrasea/Model/Entities/OrderElement.php index e5ce58f0f2..010049ab98 100644 --- a/lib/Alchemy/Phrasea/Model/Entities/OrderElement.php +++ b/lib/Alchemy/Phrasea/Model/Entities/OrderElement.php @@ -41,12 +41,13 @@ class OrderElement * @ORM\ManyToOne(targetEntity="User") * @ORM\JoinColumn(name="order_master", referencedColumnName="id") * - * @return User + * @var null|User **/ private $orderMaster; /** * @ORM\Column(type="boolean", nullable=true) + * @var bool|null */ private $deny; @@ -89,7 +90,7 @@ class OrderElement /** * Set deny * - * @param boolean $deny + * @param null|bool $deny * @return OrderElement */ public function setDeny($deny) @@ -102,7 +103,7 @@ class OrderElement /** * Get deny * - * @return boolean + * @return bool|null */ public function getDeny() { diff --git a/lib/Alchemy/Phrasea/Controller/Api/OrderController.php b/lib/Alchemy/Phrasea/Order/Controller/ApiOrderController.php similarity index 76% rename from lib/Alchemy/Phrasea/Controller/Api/OrderController.php rename to lib/Alchemy/Phrasea/Order/Controller/ApiOrderController.php index 3bd6b54e01..43bd44b134 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/OrderController.php +++ b/lib/Alchemy/Phrasea/Order/Controller/ApiOrderController.php @@ -8,21 +8,20 @@ * file that was distributed with this source code. */ -namespace Alchemy\Phrasea\Controller\Api; +namespace Alchemy\Phrasea\Order\Controller; -use Alchemy\Phrasea\Application\Helper\DispatcherAware; use Alchemy\Phrasea\Application\Helper\JsonBodyAware; -use Alchemy\Phrasea\Controller\Controller; +use Alchemy\Phrasea\Collection\Reference\CollectionReference; +use Alchemy\Phrasea\Controller\Api\Result; use Alchemy\Phrasea\Controller\RecordsRequest; use Alchemy\Phrasea\Core\Event\OrderEvent; use Alchemy\Phrasea\Core\PhraseaEvents; +use Alchemy\Phrasea\Model\Entities\BasketElement; use Alchemy\Phrasea\Model\Entities\Order; use Alchemy\Phrasea\Order\OrderElementTransformer; use Alchemy\Phrasea\Order\OrderFiller; use Alchemy\Phrasea\Order\OrderTransformer; use Alchemy\Phrasea\Record\RecordReferenceCollection; -use Assert\Assertion; -use Assert\InvalidArgumentException; use Doctrine\Common\Collections\ArrayCollection; use League\Fractal\Manager; use League\Fractal\Pagination\PagerfantaPaginatorAdapter; @@ -34,12 +33,9 @@ use Pagerfanta\Pagerfanta; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -class OrderController extends Controller +class ApiOrderController extends BaseOrderController { - use DispatcherAware; use JsonBodyAware; public function createAction(Request $request) @@ -107,20 +103,10 @@ class OrderController extends Controller */ public function showAction(Request $request, $orderId) { - try { - Assertion::integerish($orderId); - } catch (InvalidArgumentException $exception) { - throw new BadRequestHttpException($exception->getMessage(), $exception); - } + $order = $this->findOr404($orderId); $includes = $request->get('includes', []); - $order = $this->app['repo.orders']->find((int)$orderId); - - if (!$order instanceof Order) { - throw new NotFoundHttpException(sprintf('Order "%d" was not found', (int) $orderId)); - } - if ($order->getUser()->getId() !== $this->getAuthenticatedUser()->getId()) { throw new AccessDeniedHttpException(sprintf('Cannot access order "%d"', $order->getId())); } @@ -130,6 +116,34 @@ class OrderController extends Controller return $this->returnResourceResponse($request, $includes, $resource); } + public function acceptElementsAction(Request $request, $orderId) + { + $elementIds = $this->fetchElementIdsFromRequest($request); + + $elements = $this->doAcceptElements($orderId, $elementIds, $this->getAuthenticatedUser()); + + $resource = new Collection($elements, function (BasketElement $element) { + return [ + 'id' => $element->getId(), + 'created' => $element->getCreated(), + 'databox_id' => $element->getSbasId(), + 'record_id' => $element->getRecordId(), + 'index' => $element->getOrd(), + ]; + }); + + return $this->returnResourceResponse($request, [], $resource); + } + + public function denyElementsAction(Request $request, $orderId) + { + $elementIds = $this->fetchElementIdsFromRequest($request); + + $this->doDenyElements($orderId, $elementIds, $this->getAuthenticatedUser()); + + return Result::create($request, [])->createResponse(); + } + /** * @param array $records * @return \record_adapter[] @@ -188,4 +202,21 @@ class OrderController extends Controller return Result::create($request, $fractal->createData($resource)->toArray())->createResponse(); } + + /** + * @param Request $request + * @return array + */ + private function fetchElementIdsFromRequest(Request $request) + { + $data = $this->decodeJsonBody($request, 'orders.json#/definitions/order_element_collection'); + + $elementIds = []; + + foreach ($data as $elementId) { + $elementIds[] = $elementId->id; + } + + return $elementIds; + } } diff --git a/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php b/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php new file mode 100644 index 0000000000..eaee31a899 --- /dev/null +++ b/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php @@ -0,0 +1,206 @@ +app['repo.orders']; + } + + /** + * @return OrderElementRepository + */ + protected function getOrderElementRepository() + { + return $this->app['repo.order-elements']; + } + + /** + * @param int $orderId + * @return Order + */ + protected function findOr404($orderId) + { + if (null === $order = $this->getOrderRepository()->find($orderId)) { + throw new NotFoundHttpException('Order not found'); + } + + return $order; + } + + /** + * @param int $orderId + * @param array $elementIds + * @param User $acceptor + * @return OrderElement[] + */ + protected function findRequestedElements($orderId, $elementIds, User $acceptor) + { + try { + Assertion::isArray($elementIds); + } catch (\Exception $exception) { + throw new BadRequestHttpException('Improper request', $exception); + } + + $elements = $this->getOrderElementRepository()->findBy([ + 'id' => $elementIds, + 'order' => $orderId, + ]); + + if (count($elements) !== count($elementIds)) { + throw new NotFoundHttpException(sprintf('At least one requested element does not exists or does not belong to order "%s"', $orderId)); + } + + if (!$this->getOrderValidator()->isGrantedValidation($acceptor, $elements)) { + throw new AccessDeniedHttpException('At least one element is in a collection you have no access to.'); + } + + return $elements; + } + + /** + * @return OrderValidator + */ + protected function getOrderValidator() + { + return $this->app['validator.order']; + } + + /** + * @param int $order_id + * @param array $elementIds + * @param User $acceptor + * @return BasketElement[] + */ + protected function doAcceptElements($order_id, $elementIds, User $acceptor) + { + $elements = $this->findRequestedElements($order_id, $elementIds, $acceptor); + $order = $this->findOr404($order_id); + + $basket = $this->app['provider.order_basket']->provideBasketForOrderAndUser($order, $acceptor); + + $partialOrder = new PartialOrder($order, $elements); + + $orderValidator = $this->getOrderValidator(); + + $basketElements = $orderValidator->createBasketElements($partialOrder); + $this->assertRequestedElementsWereNotAlreadyAdded($basket, $basketElements); + + $orderValidator->accept($acceptor, $partialOrder); + $orderValidator->grantHD($basket->getUser(), $basketElements); + + try { + $manager = $this->getEntityManager(); + + if (!empty($basketElements)) { + foreach ($basketElements as $element) { + $basket->addElement($element); + $manager->persist($element); + } + + $this->dispatch(PhraseaEvents::ORDER_DELIVER, new OrderDeliveryEvent($order, $acceptor, count($basketElements))); + } + + $manager->persist($basket); + $manager->persist($order); + $manager->flush(); + } catch (\Exception $e) { + // I don't know why only basket persistence is not checked + } + + return $basketElements; + } + + /** + * @param int $order_id + * @param array $elementIds + * @param User $acceptor + * @return OrderElement[] + */ + protected function doDenyElements($order_id, $elementIds, User $acceptor) + { + $elements = $this->findRequestedElements($order_id, $elementIds, $acceptor); + $order = $this->findOr404($order_id); + + $this->getOrderValidator()->deny($acceptor, new PartialOrder($order, $elements)); + + try { + if (!empty($elements)) { + $this->dispatch(PhraseaEvents::ORDER_DENY, new OrderDeliveryEvent($order, $acceptor, count($elements))); + } + + $manager = $this->getEntityManager(); + $manager->persist($order); + $manager->flush(); + } catch (\Exception $e) { + // Don't know why this is ignored + } + + return $elements; + } + + /** + * @param Basket $basket + * @param BasketElement[] $elements + */ + protected function assertRequestedElementsWereNotAlreadyAdded(Basket $basket, $elements) + { + if ($basket->getElements()->isEmpty()) { + return; + } + + $references = new RecordReferenceCollection(); + + foreach ($elements as $element) { + $reference = RecordReference::createFromDataboxIdAndRecordId($element->getSbasId(), $element->getRecordId()); + + $references->addRecordReference($reference); + } + + $groups = $references->groupPerDataboxId(); + + foreach ($basket->getElements() as $element) { + if (isset($groups[$element->getSbasId()][$element->getRecordId()])) { + throw new ConflictHttpException('Some records have already been handled'); + } + } + } +} diff --git a/lib/Alchemy/Phrasea/Controller/Prod/OrderController.php b/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php similarity index 58% rename from lib/Alchemy/Phrasea/Controller/Prod/OrderController.php rename to lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php index 06527ee501..03472510c8 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/OrderController.php +++ b/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php @@ -1,5 +1,5 @@ request->get('use', ''); - $order = new OrderEntity(); + $order = new Order(); $order->setUser($this->getAuthenticatedUser()); $order->setDeadline($deadLine); $order->setOrderUsage($orderUsage); @@ -136,10 +127,7 @@ class OrderController extends Controller */ public function displayOneOrder($order_id) { - $order = $this->getOrderRepository()->find($order_id); - if (null === $order) { - throw new NotFoundHttpException('Order not found'); - } + $order = $this->findOr404($order_id); return $this->render('prod/orders/order_item.html.twig', [ 'order' => $order, @@ -155,62 +143,12 @@ class OrderController extends Controller */ public function sendOrder(Request $request, $order_id) { - $success = false; - /** @var Order $order */ - if (null === $order = $this->getOrderRepository()->find($order_id)) { - throw new NotFoundHttpException('Order not found'); - } + $elementIds = $request->request->get('elements', []); + $acceptor = $this->getAuthenticatedUser(); - $manager = $this->getEntityManager(); - $basket = $order->getBasket(); - if (null === $basket) { - $basket = new Basket(); - $basket->setName($this->app->trans('Commande du %date%', [ - '%date%' => $order->getCreatedOn()->format('Y-m-d'), - ])); - $basket->setUser($order->getUser()); - $basket->setPusher($this->getAuthenticatedUser()); + $basketElements = $this->doAcceptElements($order_id, $elementIds, $acceptor); - $manager->persist($basket); - $manager->flush(); - } - - $n = 0; - $elements = $request->request->get('elements', []); - foreach ($order->getElements() as $orderElement) { - if (in_array($orderElement->getId(), $elements)) { - $sbas_id = \phrasea::sbasFromBas($this->app, $orderElement->getBaseId()); - $record = new \record_adapter($this->app, $sbas_id, $orderElement->getRecordId()); - - $basketElement = new BasketElement(); - $basketElement->setRecord($record); - $basketElement->setBasket($basket); - - $orderElement->setOrderMaster($this->getAuthenticatedUser()); - $orderElement->setDeny(false); - $orderElement->getOrder()->setBasket($basket); - - $basket->addElement($basketElement); - - $n++; - $this->getAclForUser($basket->getUser())->grant_hd_on($record, $this->getAuthenticatedUser(), 'order'); - } - } - - try { - if ($n > 0) { - $order->setTodo($order->getTodo() - $n); - $this->dispatch(PhraseaEvents::ORDER_DELIVER, new OrderDeliveryEvent($order, $this->getAuthenticatedUser(), $n)); - } - $success = true; - - // There was a basketElement persist here. Seems useless as all entities are managed. - $manager->persist($basket); - $manager->persist($order); - $manager->flush(); - } catch (\Exception $e) { - - } + $success = !empty($basketElements); if ('json' === $request->getRequestFormat()) { return $this->app->json([ @@ -237,38 +175,12 @@ class OrderController extends Controller */ public function denyOrder(Request $request, $order_id) { - $success = false; - /** @var Order $order */ - $order = $this->getOrderRepository()->find($order_id); - if (null === $order) { - throw new NotFoundHttpException('Order not found'); - } + $elementIds = $request->request->get('elements', []); + $acceptor = $this->getAuthenticatedUser(); - $n = 0; - $elements = $request->request->get('elements', []); - $manager = $this->getEntityManager(); - foreach ($order->getElements() as $orderElement) { - if (in_array($orderElement->getId(),$elements)) { - $orderElement->setOrderMaster($this->getAuthenticatedUser()); - $orderElement->setDeny(true); + $elements = $this->doDenyElements($order_id, $elementIds, $acceptor); - $manager->persist($orderElement); - $n++; - } - } - - try { - if ($n > 0) { - $order->setTodo($order->getTodo() - $n); - $this->dispatch(PhraseaEvents::ORDER_DENY, new OrderDeliveryEvent($order, $this->getAuthenticatedUser(), $n)); - } - $success = true; - - $manager->persist($order); - $manager->flush(); - } catch (\Exception $e) { - - } + $success = !empty($elements); if ('json' === $request->getRequestFormat()) { return $this->app->json([ @@ -286,11 +198,5 @@ class OrderController extends Controller ]); } - /** - * @return OrderRepository - */ - private function getOrderRepository() - { - return $this->app['repo.orders']; - } + } diff --git a/lib/Alchemy/Phrasea/Order/OrderBasketProvider.php b/lib/Alchemy/Phrasea/Order/OrderBasketProvider.php new file mode 100644 index 0000000000..8a442bb9e0 --- /dev/null +++ b/lib/Alchemy/Phrasea/Order/OrderBasketProvider.php @@ -0,0 +1,58 @@ +manager = $manager; + $this->translator = $translator; + } + + public function provideBasketForOrderAndUser(Order $order, User $acceptor) + { + $basket = $order->getBasket(); + + if (null === $basket) { + $basket = new Basket(); + $basket->setName($this->translator->trans('Commande du %date%', [ + '%date%' => $order->getCreatedOn()->format('Y-m-d'), + ])); + + $order->setBasket($basket); + + $basket->setUser($order->getUser()); + $basket->setPusher($acceptor); + + $this->manager->persist($basket); + $this->manager->flush($basket); + } + + return $basket; + } +} diff --git a/lib/Alchemy/Phrasea/Order/OrderFiller.php b/lib/Alchemy/Phrasea/Order/OrderFiller.php index ed38750f5b..eec1347f3e 100644 --- a/lib/Alchemy/Phrasea/Order/OrderFiller.php +++ b/lib/Alchemy/Phrasea/Order/OrderFiller.php @@ -34,7 +34,7 @@ class OrderFiller } /** - * @param \record_adapter[] $records + * @param \record_adapter[]|\Traversable $records */ public function assertAllRecordsHaveOrderMaster($records) { @@ -62,7 +62,7 @@ class OrderFiller } /** - * @param \record_adapter[] $records + * @param \record_adapter[]|\Traversable $records * @param Order $order */ public function fillOrder(Order $order, $records) diff --git a/lib/Alchemy/Phrasea/Order/OrderValidator.php b/lib/Alchemy/Phrasea/Order/OrderValidator.php new file mode 100644 index 0000000000..d0e919040f --- /dev/null +++ b/lib/Alchemy/Phrasea/Order/OrderValidator.php @@ -0,0 +1,176 @@ +appbox = $appbox; + $this->repository = $repository; + } + + /** + * @param User $acceptor + * @param OrderElement[] $elements + * @return bool + */ + public function isGrantedValidation(User $acceptor, $elements) + { + $acceptableCollections = $this->getAclForUser($acceptor)->getOrderMasterCollectionsBaseIds(); + + $elementsCollections = []; + + foreach ($elements as $element) { + $elementsCollections[$element->getBaseId()] = true; + } + + return empty(array_diff(array_keys($elementsCollections), $acceptableCollections)); + } + + /** + * @param PartialOrder $order + * @return BasketElement[] + */ + public function createBasketElements(PartialOrder $order) + { + $basketElements = []; + + $references = $this->getRecordReferenceCollection($order); + + foreach ($references->toRecords($this->appbox) as $record) { + $basketElement = new BasketElement(); + $basketElement->setRecord($record); + + $basketElements[] = $basketElement; + } + + return $basketElements; + } + + /** + * @param User $acceptor + * @param PartialOrder $order + */ + public function accept(User $acceptor, PartialOrder $order) + { + $this->acceptOrDenyPartialOrder($acceptor, $order, self::VALIDATION_ACCEPT); + } + + /** + * @param User $acceptor + * @param PartialOrder $order + */ + public function deny(User $acceptor, PartialOrder $order) + { + $this->acceptOrDenyPartialOrder($acceptor, $order, self::VALIDATION_DENY); + } + + /** + * @param User $user + * @param BasketElement[] $elements + */ + public function grantHD(User $user, $elements) + { + Assertion::allIsInstanceOf($elements, BasketElement::class); + + $acl = $this->getAclForUser($user); + + foreach ($elements as $element) { + $recordReference = RecordReference::createFromDataboxIdAndRecordId( + $element->getSbasId(), + $element->getRecordId() + ); + + $acl->grant_hd_on($recordReference, $user, 'order'); + } + } + + /** + * @param PartialOrder $order + * @return RecordReferenceCollection + */ + private function getRecordReferenceCollection(PartialOrder $order) + { + $databoxIdMap = []; + + foreach ($this->repository->findMany($order->getBaseIds()) as $collectionReference) { + $databoxIdMap[$collectionReference->getBaseId()] = $collectionReference->getDataboxId(); + } + + $references = new RecordReferenceCollection(); + + foreach ($order->getElements() as $orderElement) { + if (!isset($databoxIdMap[$orderElement->getBaseId()])) { + throw new \RuntimeException('At least one collection was not found.'); + } + + $references->addRecordReference(RecordReference::createFromDataboxIdAndRecordId( + $databoxIdMap[$orderElement->getBaseId()], + $orderElement->getRecordId() + )); + } + + return $references; + } + + /** + * @param User $acceptor + * @param PartialOrder $order + * @param bool $deny + */ + private function acceptOrDenyPartialOrder(User $acceptor, PartialOrder $order, $deny) + { + $elements = $order->getElements(); + + if (empty($elements)) { + return; + } + + $decrementCount = 0; + + foreach ($elements as $element) { + $element->setOrderMaster($acceptor); + if (null === $element->getDeny()) { + ++$decrementCount; + } + $element->setDeny($deny); + } + + if ($decrementCount) { + $order->getOrder()->decrementTodo($decrementCount); + } + } +} diff --git a/lib/Alchemy/Phrasea/Order/PartialOrder.php b/lib/Alchemy/Phrasea/Order/PartialOrder.php new file mode 100644 index 0000000000..4eb094bef2 --- /dev/null +++ b/lib/Alchemy/Phrasea/Order/PartialOrder.php @@ -0,0 +1,76 @@ +order = $order; + + $this->elements = []; + + foreach ($elements as $element) { + if (null === $element->getOrder() || $element->getOrder()->getId() !== $order->getId()) { + throw new \InvalidArgumentException('Elements should belong to same order'); + } + + $this->elements[$element->getId()] = $element; + } + } + + /** + * @return Order + */ + public function getOrder() + { + return $this->order; + } + + /** + * @return OrderElement[] + */ + public function getElements() + { + return $this->elements; + } + + public function getBaseIds() + { + $baseIds = []; + + foreach ($this->elements as $element) { + $baseIds[$element->getBaseId()] = true; + } + + return array_keys($baseIds); + } +} diff --git a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php index 8335c4db90..802d4a9e67 100644 --- a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php +++ b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php @@ -15,8 +15,6 @@ use Assert\Assertion; class RecordReferenceCollection implements \IteratorAggregate { - private $groups; - /** * @param array $records * @return RecordReferenceCollection @@ -43,16 +41,27 @@ class RecordReferenceCollection implements \IteratorAggregate */ private $references = []; + /** + * @var null|array + */ + private $groups; + /** * @param RecordReferenceInterface[] $references */ - public function __construct($references) + public function __construct($references = []) { Assertion::allIsInstanceOf($references, RecordReferenceInterface::class); $this->references = $references instanceof \Traversable ? iterator_to_array($references) : $references; } + public function addRecordReference(RecordReferenceInterface $reference) + { + $this->references[] = $reference; + $this->groups = null; + } + public function getIterator() { return new \ArrayIterator($this->references); diff --git a/lib/classes/ACL.php b/lib/classes/ACL.php index b28c134af6..649a781528 100644 --- a/lib/classes/ACL.php +++ b/lib/classes/ACL.php @@ -10,15 +10,12 @@ */ use Alchemy\Phrasea\Application; -use Alchemy\Phrasea\Model\Entities\User; -use Doctrine\DBAL\DBALException; -use Alchemy\Phrasea\Model\RecordInterface; - -use Alchemy\Phrasea\Core\Event\Acl\AclEvents; +use Alchemy\Phrasea\Collection\Reference\CollectionReferenceCollection; use Alchemy\Phrasea\Core\Event\Acl\AccessPeriodChangedEvent; use Alchemy\Phrasea\Core\Event\Acl\AccessToBaseGrantedEvent; use Alchemy\Phrasea\Core\Event\Acl\AccessToBaseRevokedEvent; use Alchemy\Phrasea\Core\Event\Acl\AccessToSbasGrantedEvent; +use Alchemy\Phrasea\Core\Event\Acl\AclEvents; use Alchemy\Phrasea\Core\Event\Acl\DownloadQuotasOnBaseChangedEvent; use Alchemy\Phrasea\Core\Event\Acl\DownloadQuotasOnBaseRemovedEvent; use Alchemy\Phrasea\Core\Event\Acl\DownloadQuotasResetEvent; @@ -26,6 +23,10 @@ use Alchemy\Phrasea\Core\Event\Acl\MasksOnBaseChangedEvent; use Alchemy\Phrasea\Core\Event\Acl\RightsToBaseChangedEvent; use Alchemy\Phrasea\Core\Event\Acl\RightsToSbasChangedEvent; use Alchemy\Phrasea\Core\Event\Acl\SysadminChangedEvent; +use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Model\RecordInterface; +use Alchemy\Phrasea\Model\RecordReferenceInterface; +use Doctrine\DBAL\DBALException; class ACL implements cache_cacheableInterface @@ -52,51 +53,40 @@ class ACL implements cache_cacheableInterface ]; /** - * - * @var user + * @var User */ protected $user; /** - * - * @var Array + * @var array */ protected $_rights_sbas; /** - * - * @var Array + * @var array */ protected $_rights_bas; /** - * - * @var Array + * @var array */ protected $_rights_records_document; /** - * - * @var Array + * @var array */ protected $_rights_records_preview; /** - * - * @var Array + * @var array */ protected $_limited; /** - * - * @var boolean + * @var bool */ protected $is_admin; - /** - * - * @var Array - */ protected $_global_rights = [ 'addrecord' => false, 'addtoalbum' => false, @@ -121,7 +111,6 @@ class ACL implements cache_cacheableInterface ]; /** - * * @var Application */ protected $app; @@ -140,15 +129,11 @@ class ACL implements cache_cacheableInterface * * @param User $user * @param Application $app - * - * @return \ACL */ public function __construct(User $user, Application $app) { $this->user = $user; $this->app = $app; - - return $this; } /** @@ -164,10 +149,10 @@ class ACL implements cache_cacheableInterface /** * Check if a hd grant has been received for a record * - * @param \record_adapter $record - * @return boolean + * @param RecordReferenceInterface $record + * @return bool */ - public function has_hd_grant(RecordInterface $record) + public function has_hd_grant(RecordReferenceInterface $record) { $this->load_hd_grant(); @@ -179,7 +164,7 @@ class ACL implements cache_cacheableInterface return false; } - public function grant_hd_on(RecordInterface $record, User $pusher, $action) + public function grant_hd_on(RecordReferenceInterface $record, User $pusher, $action) { $sql = 'REPLACE INTO records_rights (id, usr_id, sbas_id, record_id, document, `case`, pusher_usr_id) @@ -203,7 +188,7 @@ class ACL implements cache_cacheableInterface return $this; } - public function grant_preview_on(RecordInterface $record, User $pusher, $action) + public function grant_preview_on(RecordReferenceInterface $record, User $pusher, $action) { $sql = 'REPLACE INTO records_rights (id, usr_id, sbas_id, record_id, preview, `case`, pusher_usr_id) @@ -230,10 +215,10 @@ class ACL implements cache_cacheableInterface /** * Check if a hd grant has been received for a record * - * @param \record_adapter $record - * @return boolean + * @param RecordReferenceInterface $record + * @return bool */ - public function has_preview_grant(RecordInterface $record) + public function has_preview_grant(RecordReferenceInterface $record) { $this->load_hd_grant(); @@ -1761,24 +1746,50 @@ class ACL implements cache_cacheableInterface } /** - * Returns an array of collections on which the user is 'order master' + * Returns base ids on which user is 'order master' * * @return array */ - public function get_order_master_collections() + public function getOrderMasterCollectionsBaseIds() { $sql = 'SELECT base_id FROM basusr WHERE order_master="1" AND usr_id= :usr_id'; - $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); - $stmt->execute([':usr_id' => $this->user->getId()]); - $rs = $stmt->fetchAll(\PDO::FETCH_ASSOC); - $stmt->closeCursor(); + $result = $this->app->getApplicationBox() + ->get_connection() + ->executeQuery($sql, [':usr_id' => $this->user->getId()]) + ->fetchAll(\PDO::FETCH_ASSOC); + + + $baseIds = []; + + foreach ($result as $item) { + $baseIds[] = $item['base_id']; + } + + return $baseIds; + } + + /** + * Returns an array of collections on which the user is 'order master' + * + * @return collection[] + */ + public function get_order_master_collections() + { + $baseIds = $this->getOrderMasterCollectionsBaseIds(); + + $collectionReferences = $this->app['repo.collection-references']->findHavingOrderMaster($baseIds); + $groups = new CollectionReferenceCollection($collectionReferences); $collections = []; - foreach ($rs as $row) { - $collections[] = \collection::getByBaseId($this->app, $row['base_id']); + foreach ($groups->groupByDataboxIdAndCollectionId() as $databoxId => $group) { + foreach ($group as $collectionId => $index) { + $collections[$index] = \collection::getByCollectionId($this->app, $databoxId, $collectionId); + } } + ksort($collections); + return $collections; } diff --git a/lib/classes/collection.php b/lib/classes/collection.php index 7ac76177bb..9c56620c05 100644 --- a/lib/classes/collection.php +++ b/lib/classes/collection.php @@ -192,16 +192,17 @@ class collection implements ThumbnailedElement, cache_cacheableInterface } /** - * @param Application $app - * @param databox $databox - * @param int $collectionId + * @param Application $app + * @param databox|int $databox + * @param int $collectionId * @return collection */ - public static function getByCollectionId(Application $app, databox $databox, $collectionId) + public static function getByCollectionId(Application $app, $databox, $collectionId) { assert(is_int($collectionId)); + $databoxId = $databox instanceof databox ? $databox->get_sbas_id() : (int)$databox; - return self::getAvailableCollection($app, $databox->get_sbas_id(), $collectionId); + return self::getAvailableCollection($app, $databoxId, $collectionId); } /** diff --git a/lib/conf.d/json_schema/orders.json b/lib/conf.d/json_schema/orders.json index 9ae143cc33..9e10d09909 100644 --- a/lib/conf.d/json_schema/orders.json +++ b/lib/conf.d/json_schema/orders.json @@ -64,7 +64,7 @@ "owner_id", "created", "usage", - "records" + "elements" ] }, "order_element": { @@ -87,6 +87,23 @@ "id", "record_id" ] + }, + "order_element_id": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + }, + "required": [ + "id" + ] + }, + "order_element_collection": { + "type": "array", + "items": { + "$ref": "#/definitions/order_element_id" + } } }, "type": "object", diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Prod/OrderTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Prod/OrderTest.php index 16b555d70e..e6bb39931c 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Prod/OrderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Prod/OrderTest.php @@ -116,12 +116,11 @@ class OrderTest extends \PhraseanetAuthenticatedWebTestCase foreach ($order->getElements() as $element) { $parameters[] = $element->getId(); } - $client = $this->getClient(); - $client->request('POST', '/prod/order/' . $order->getId() . '/send/', ['elements' => $parameters]); - $this->assertTrue($client->getResponse()->isRedirect()); - $url = parse_url($client->getResponse()->headers->get('location')); + $response = $this->request('POST', '/prod/order/' . $order->getId() . '/send/', ['elements' => $parameters]); + $this->assertTrue($response->isRedirect(), 'Could not validate some elements. not a redirect'); + $url = parse_url($response->headers->get('location')); parse_str($url['query']); - $this->assertTrue( strpos($url['query'], 'success=1') === 0); + $this->assertTrue(strpos($url['query'], 'success=1') === 0, 'Validation of elements is not successful'); } /**