Merge pull request #3425 from alchemy-fr/PHRAS-3006_Port41_Front_delete_3_by_3

PHRAS-3006 #comment merge Port to 41 :  front delete 3 by 3
This commit is contained in:
Nicolas Maillat
2020-04-06 10:48:31 +02:00
committed by GitHub
10 changed files with 726 additions and 617 deletions

View File

@@ -7,6 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Controller\Prod;
use Alchemy\Phrasea\Application;
@@ -24,6 +25,7 @@ use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
use Alchemy\Phrasea\Twig\Fit;
use Alchemy\Phrasea\Twig\PhraseanetExtension;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -36,7 +38,7 @@ class RecordController extends Controller
*
* @param Request $request
*
* @return Response
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function getRecord(Request $request)
{
@@ -194,7 +196,8 @@ class RecordController extends Controller
$flatten = (bool)($request->request->get('del_children')) ? RecordsRequest::FLATTEN_YES_PRESERVE_STORIES : RecordsRequest::FLATTEN_NO;
$records = RecordsRequest::fromRequest(
$this->app,
$request,$flatten,
$request,
$flatten,
[\ACL::CANDELETERECORD]
);
@@ -224,16 +227,16 @@ class RecordController extends Controller
$manager->remove($attachedStory);
}
foreach($record->get_grouping_parents() as $story) {
foreach ($record->get_grouping_parents() as $story) {
$this->getEventDispatcher()->dispatch(PhraseaEvents::RECORD_EDIT, new RecordEdit($story));
}
$sbasId = $record->getDatabox()->get_sbas_id();
if(!array_key_exists($sbasId, $trashCollectionsBySbasId)) {
if (!array_key_exists($sbasId, $trashCollectionsBySbasId)) {
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
}
$deleted[] = $record->getId();
if($trashCollectionsBySbasId[$sbasId] !== null) {
if ($trashCollectionsBySbasId[$sbasId] !== null) {
if($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
// record is already in trash so delete it
$this->getEventDispatcher()->dispatch(RecordEvents::DELETE, new DeleteEvent($record));
@@ -280,35 +283,69 @@ class RecordController extends Controller
* Delete a record or a list of records
*
* @param Request $request
* @return Response
* @return string html
*/
public function whatCanIDelete(Request $request)
{
$viewParms = [];
// pre-count records that would be trashed/deleted when the "deleted children" will be un-checked
$records = RecordsRequest::fromRequest(
$this->app,
$request,
!!$request->request->get('del_children'),
RecordsRequest::FLATTEN_NO,
[\ACL::CANDELETERECORD]
);
$filteredRecord = $this->filterRecordToDelete($records);
$filteredRecords = $this->filterRecordToDelete($records);
return $this->app->json([
'renderView' => $this->render('prod/actions/delete_records_confirm.html.twig', [
'records' => $records,
'filteredRecord' => $filteredRecord
]),
'filteredRecord' => $filteredRecord
]);
$viewParms['parents_only'] = [
'records' => $records,
'trashableCount' => count($filteredRecords['trash']),
'deletableCount' => count($filteredRecords['delete'])
];
// pre-count records that would be trashed/deleted when the "deleted children" will be checked
//
$records = RecordsRequest::fromRequest(
$this->app,
$request,
RecordsRequest::FLATTEN_YES_PRESERVE_STORIES,
[\ACL::CANDELETERECORD]
);
$filteredRecords = $this->filterRecordToDelete($records);
$viewParms['with_children'] = [
'records' => $records,
'trashableCount' => count($filteredRecords['trash']),
'deletableCount' => count($filteredRecords['delete'])
];
return $this->render(
'prod/actions/delete_records_confirm.html.twig',
$viewParms
);
}
/**
* classifies records in two groups (does NOT delete anything)
* - 'trash' : the record can go to trash because the db has a "_TRASH_" coll, and the record is not already into it
* - 'delete' : the record would be deleted because the db has no trash, or the record is already trashed
*
* @param RecordsRequest $records
* @return array
*/
private function filterRecordToDelete(RecordsRequest $records)
{
$ret = [
'trash' => [],
'delete' => []
];
$trashCollectionsBySbasId = [];
$goingToTrash = [];
$delete = [];
foreach ($records as $record) {
/** @var \record_adapter $record */
$sbasId = $record->getDatabox()->get_sbas_id();
if (!array_key_exists($sbasId, $trashCollectionsBySbasId)) {
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
@@ -316,21 +353,20 @@ class RecordController extends Controller
if ($trashCollectionsBySbasId[$sbasId] !== null) {
if ($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
// record is already in trash
$delete[] = $record;
$ret['delete'][] = $record;
}
else {
// will be moved to trash
$goingToTrash[] = $record;
$ret['trash'][] = $record;
}
}
else {
// trash does not exist
$delete[] = $record;
$ret['delete'][] = $record;
}
}
//check if all values in array are true
//return (!in_array(false, $goingToTrash, true));
return ['trash' => $goingToTrash, 'delete' => $delete];
return $ret;
}
/**
@@ -338,7 +374,8 @@ class RecordController extends Controller
*
* @param Request $request
*
* @return Response
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \Alchemy\Phrasea\Cache\Exception
*/
public function renewUrl(Request $request)
{

View File

@@ -11,6 +11,7 @@
namespace Alchemy\Phrasea\Controller;
use Alchemy\Phrasea\Model\Converter\BasketConverter;
use Alchemy\Phrasea\Model\Entities\Basket;
use Doctrine\Common\Collections\ArrayCollection;
use Alchemy\Phrasea\Application;
@@ -21,6 +22,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RecordsRequest extends ArrayCollection
{
protected $isSingleStory = false;
protected $rejected;
protected $received;
protected $basket;
protected $databoxes;
@@ -31,30 +33,38 @@ class RecordsRequest extends ArrayCollection
const FLATTEN_YES_PRESERVE_STORIES = 'preserve';
/**
* Constructor
* RecordsRequest Constructor
*
* @param array $elements
* @param ArrayCollection $rejected
* @param ArrayCollection $received
* @param Basket $basket
* @param Basket|null $basket
* @param Boolean $flatten
*/
public function __construct(array $elements, ArrayCollection $received, Basket $basket = null, $flatten = self::FLATTEN_NO)
public function __construct(array $elements, ArrayCollection $rejected, ArrayCollection $received, Basket $basket = null, $flatten = self::FLATTEN_NO)
{
parent::__construct($elements);
$this->received = $received;
$this->rejected = $rejected;
$this->basket = $basket;
$this->isSingleStory = ($flatten !== self::FLATTEN_YES && 1 === count($this) && $this->first()->isStory());
$this->isSingleStory = ($flatten !== self::FLATTEN_YES && count($this) === 1 && $this->first()->isStory());
if (self::FLATTEN_NO !== $flatten) {
if ($flatten !== self::FLATTEN_NO) {
$to_remove = [];
/** @var record_adapter $record */
foreach ($this as $key => $record) {
if ($record->isStory()) {
if (self::FLATTEN_YES === $flatten) {
if ($flatten === self::FLATTEN_YES) {
// simple flatten : remove the story
$to_remove[] = $key;
}
foreach ($record->getChildren() as $child) {
$this->set($child->getId(), $child);
try {
foreach ($record->getChildren() as $child) {
$this->set($child->getId(), $child);
}
} catch (\Exception $e) {
// getChildren will no fail since record IS a story
}
}
}
@@ -106,7 +116,7 @@ class RecordsRequest extends ArrayCollection
/** @var \record_adapter $record */
foreach ($this as $record) {
if (! isset($this->collections[$record->getBaseId()])) {
$this->collections[$record->getBaseId()] = $record->get_collection();
$this->collections[$record->getBaseId()] = $record->getCollection();
}
}
@@ -126,6 +136,16 @@ class RecordsRequest extends ArrayCollection
return $this->received;
}
/**
* Return all rejected records
*
* @return \record_adapter[]|ArrayCollection
*/
public function rejected()
{
return $this->rejected;
}
/**
* Return basket entity if provided, null otherwise
*
@@ -201,15 +221,18 @@ class RecordsRequest extends ArrayCollection
* @param boolean $flattenStories
* @param array $rightsColl
* @param array $rightsDatabox
* @return RecordsRequest|\record_adapter[]
* @return RecordsRequest
* @throws \Alchemy\Phrasea\Cache\Exception
*/
public static function fromRequest(Application $app, Request $request, $flattenStories = self::FLATTEN_NO, array $rightsColl = [], array $rightsDatabox = [])
{
$elements = $received = [];
$received = [];
$basket = null;
if ($request->get('ssel')) {
$basket = $app['converter.basket']->convert($request->get('ssel'));
/** @var BasketConverter $basketConverter */
$basketConverter = $app['converter.basket'];
$basket = $basketConverter->convert($request->get('ssel'));
$app['acl.basket']->hasAccess($basket, $app->getAuthenticatedUser());
foreach ($basket->getElements() as $basket_element) {
@@ -240,35 +263,56 @@ class RecordsRequest extends ArrayCollection
}
}
// fill an array with records from flattened stories
$elements = $received;
$to_remove = [];
foreach ($elements as $id => $record) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_access_to_record($record)) {
$to_remove[] = $id;
continue;
}
foreach ($rightsColl as $right) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_right_on_base($record->get_base_id(), $right)) {
$to_remove[] = $id;
continue;
}
}
foreach ($rightsDatabox as $right) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_right_on_sbas($record->get_sbas_id(), $right)) {
$to_remove[] = $id;
continue;
if ($flattenStories !== self::FLATTEN_NO) {
/** @var record_adapter $record */
foreach ($received as $key => $record) {
if ($record->isStory()) {
if ($flattenStories === self::FLATTEN_YES) {
// simple flatten : remove the story from elements
unset($elements[$key]);
}
foreach ($record->getChildren() as $child) {
$elements[$child->getId()] = $child;
}
}
}
}
foreach ($to_remove as $id) {
unset($elements[$id]);
// apply rights filter, remove from elements if no rights
$rejected = [];
$acl = $app->getAclForUser($app->getAuthenticatedUser());
foreach ($elements as $key => $record) {
// any false or unknown right will throw exception and the record will be rejected
try {
if (!$acl->has_access_to_record($record)) {
throw new \Exception();
}
foreach ($rightsColl as $right) {
if (!$acl->has_right_on_base($record->getBaseId(), $right)) {
throw new \Exception();
}
}
foreach ($rightsDatabox as $right) {
if (!$acl->has_right_on_sbas($record->getDataboxId(), $right)) {
throw new \Exception();
}
}
}
catch (\Exception $e) {
$rejected[$key] = $record;
}
}
// remove rejected from elements
foreach ($rejected as $key => $record) {
unset($elements[$key]);
}
return new static($elements, new ArrayCollection($received), $basket, $flattenStories);
// flattening is already done
return new static($elements, new ArrayCollection($rejected), new ArrayCollection($received), $basket, self::FLATTEN_NO);
}
}

View File

@@ -31,7 +31,8 @@ class BasketConverter implements ConverterInterface
*/
public function convert($id)
{
if (null === $basket = $this->repository->find((int) $id)) {
/** @var Basket $basket */
if ( ($basket = $this->repository->find((int) $id)) === null) {
throw new NotFoundHttpException(sprintf('Basket %s not found.', $id));
}

View File

@@ -47,6 +47,10 @@ class ApiOrderController extends BaseOrderController
use FilesystemAware;
use JsonBodyAware;
/**
* @param Request $request
* @return Response
*/
public function createAction(Request $request)
{
$data = $this->decodeJsonBody($request, 'orders.json#/definitions/order_request');
@@ -54,7 +58,13 @@ class ApiOrderController extends BaseOrderController
$availableRecords = $this->toRequestedRecords($data->data->records);
$records = $this->filterOrderableRecords($availableRecords);
$recordRequest = new RecordsRequest($records, new ArrayCollection($availableRecords), null, RecordsRequest::FLATTEN_YES);
$recordRequest = new RecordsRequest(
$records, // orderable records
new ArrayCollection([]), // rejected (rights)
new ArrayCollection($availableRecords), // all records from request
null, // basket
RecordsRequest::FLATTEN_YES // orderable records is stories + children
);
$filler = new OrderFiller($this->app['repo.collection-references'], $this->app['orm.em']);
@@ -62,7 +72,13 @@ class ApiOrderController extends BaseOrderController
$order = new Order();
$order->setUser($this->getAuthenticatedUser());
$order->setDeadline(new \DateTime($data->data->deadline, new \DateTimeZone('UTC')));
try {
$order->setDeadline(new \DateTime()); // safe value in case of data->deadline is invalid
$order->setDeadline(new \DateTime($data->data->deadline, new \DateTimeZone('UTC')));
}
catch (\Exception $e) {
// no-op, will end-up with safe value
}
$order->setOrderUsage($data->data->usage);
$order->setNotificationMethod(Order::NOTIFY_WEBHOOK);
@@ -139,8 +155,9 @@ class ApiOrderController extends BaseOrderController
}
/**
* @param int $orderId
* @param $orderId
* @return Response
* @throws \Exception
*/
public function getArchiveAction($orderId)
{
@@ -162,7 +179,12 @@ class ApiOrderController extends BaseOrderController
$user = $this->getAuthenticatedUser();
$subdefs = $this->findDataboxSubdefNames();
$exportData = $export->prepare_export($user, $this->getFilesystem(), $subdefs, true, true);
try {
$exportData = $export->prepare_export($user, $this->getFilesystem(), $subdefs, true, true);
}
catch (\Exception $e) {
throw new NotFoundHttpException(sprintf('No archive could be downloaded for Order "%d"', $order->getId()));
}
/** @var Token $token */
$token = $this->app['manipulator.token']->createDownloadToken($user, serialize($exportData));
@@ -248,8 +270,13 @@ class ApiOrderController extends BaseOrderController
$filtered = [];
foreach ($records as $index => $record) {
if (!$record->isStory() && $acl->has_right_on_base($record->getBaseId(), \ACL::CANCMD)) {
$filtered[$index] = $record;
try {
if ($acl->has_right_on_base($record->getBaseId(), \ACL::CANCMD)) {
$filtered[$index] = $record;
}
}
catch (\Exception $e) {
// will NOT happen since \ACL::CANCMD IS a known right
}
}

View File

@@ -65,7 +65,7 @@
"normalize-css": "^2.1.0",
"npm": "^6.0.0",
"npm-modernizr": "^2.8.3",
"phraseanet-production-client": "0.34.160-d",
"phraseanet-production-client": "0.34.162-d",
"requirejs": "^2.3.5",
"tinymce": "^4.0.28",
"underscore": "^1.8.3",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
<file date="2020-03-17T12:44:42Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<file date="2020-04-03T07:42:51Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<header>
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
@@ -9,9 +9,9 @@
<trans-unit id="96f0767cb7ea65a7f86c8c9432e80d16cf9d8680" resname="Please provide the same passwords." approved="yes">
<source>Please provide the same passwords.</source>
<target state="translated">Bitte geben Sie diesselbe Passwörter ein.</target>
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
<jms:reference-file line="44">Form/Login/PhraseaRecoverPasswordForm.php</jms:reference-file>
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
</trans-unit>
<trans-unit id="90b8c9717bb7ed061dbf20fe1986c8b8593d43d4" resname="The token provided is not valid anymore" approved="yes">
<source>The token provided is not valid anymore</source>

View File

@@ -1,67 +1,14 @@
{% set nbReceived = records.received().count() %}
{% set nbEligibleDocuments = records.count() %}
{% set nbTrash = filteredRecord.trash|length %}
{% set nbDelete = filteredRecord.delete|length %}
{% if nbEligibleDocuments > 0 %}
{% if nbReceived != records.count() %}
<div class="well-small" style="text-align:center;">
<span class="label label-info">{{ "You do not have rights to remove all selected documents. Are you sure ?" | trans }}</span>
</div>
{% endif %}
<form id="delete-record-form" style="margin: 0;" method="POST" action="{{ path('record_delete') }}">
<input type="hidden" value="{{ records.serializedList() }}" name="lst" />
{% if nbTrash > 0 %}
<div class="well-small label-important"
style="background-color: #ffef22;">
<div class="dialog-left-section">
<img src="/assets/common/images/icons/icon_collection_bin.png"/>
</div>
<div class="dialog-right-section" style="margin-top: 8px;">
<span>{{ nbTrash }} {{ "prod:app trash: record-move-to-trash" | trans }}</span>
</div>
{% if records.stories().count() %}
<label class="checkbox story">
<input type="checkbox" id="del_children" name="del_children"
value="1"> {{ "prod:app trash: also-move-record" | trans }}
</label>
{% endif %}
</div>
{% endif %}
{% if nbDelete > 0 %}
<div class="well-small label-important"
style="background-color: #ed1c24;">
<div class="dialog-left-section">
<img src="/assets/common/images/icons/icon_empty_bin.png"/>
</div>
<div class="dialog-right-section">
<span>{{ nbDelete }} {{ "prod:app trash: record-delete" | trans }}</span>
</div>
{% if records.stories().count() %}
<label class="checkbox story">
<input type="checkbox" id="del_children" name="del_children"
value="1"> {{ "Also delete records that rely on groupings." | trans }}
</label>
{% endif %}
</div>
{% endif %}
<div class="form-actions" style="background-color:transparent;">
<button type="button" class="btn btn-danger submiter">{{ "Ok" | trans }}</button>
<button type="button" class="btn cancel">{{ "Cancel" | trans }}</button>
<span class="form-action-loader" style="display:none;">
<img src="/assets/common/images/icons/loader000.gif"/>
</span>
</div>
</form>
{% elseif nbReceived == 0 %}
<div class="well-small" style="text-align:center;">
<span class="label label-important">{{ "No document selected" | trans }}</span>
</div>
{% else %}
<div class="well-small" style="text-align:center;">
<span class="label label-info">{{ "You do not have rights to remove selected documents" | trans }}</span>
</div>
{% if with_children.records.stories().count() > 0 %}
<label class="checkbox">
<input type="checkbox" id="del_children" name="del_children"
value="1"> {{ "Also delete records that rely on groupings." | trans }}
</label>
{% endif %}
<div id="delete_records_parent_only">
{{ include('prod/actions/delete_records_confirm_form.html.twig', parents_only) }}
</div>
<div id="delete_records_with_children">
{{ include('prod/actions/delete_records_confirm_form.html.twig', with_children) }}
</div>

View File

@@ -0,0 +1,48 @@
{% if records.count() > 0 %}
{% if records.rejected().count() > 0 %}
<div class="well-small" style="text-align:center;">
{{ "You do not have rights to remove all selected documents. Are you sure ?" | trans }}
</div>
{% endif %}
<form id="delete-record-form" style="margin: 0;" method="POST" action="{{ path('record_delete') }}">
<input type="hidden" value="{{ records.serializedList() }}" name="lst"/>
{% if trashableCount > 0 %}
<div class="well-small label-important"
style="background-color: #ffef22;">
<div class="dialog-left-section">
<img src="/assets/common/images/icons/icon_collection_bin.png"/>
</div>
<div class="dialog-right-section" style="margin-top: 8px;">
<span class="to_trash_count">{{ trashableCount }}</span> {{ "prod:app trash: record-move-to-trash" | trans }}
</div>
</div>
{% endif %}
{% if deletableCount > 0 %}
<div class="well-small label-important"
style="background-color: #ed1c24;">
<div class="dialog-left-section">
<img src="/assets/common/images/icons/icon_empty_bin.png"/>
</div>
<div class="dialog-right-section">
<span class="to_delete_count">{{ deletableCount }}</span> {{ "prod:app trash: record-to-delete" | trans }}
</div>
</div>
{% endif %}
<div class="form-actions" style="background-color:transparent;">
<button type="button" class="btn btn-danger submiter">{{ "Ok" | trans }}</button>
<button type="button" class="btn cancel">{{ "Cancel" | trans }}</button>
<span class="form-action-loader" style="display:none;">
<img src="/assets/common/images/icons/loader000.gif"/>
</span>
</div>
</form>
{% elseif records.received().count() == 0 %}
<div class="well-small" style="text-align:center;">
<span class="label label-important">{{ "No document selected" | trans }}</span>
</div>
{% else %}
<div class="well-small" style="text-align:center;">
<span class="label label-info">{{ "You do not have rights to remove selected documents" | trans }}</span>
</div>
{% endif %}

View File

@@ -7577,10 +7577,10 @@ phraseanet-common@^0.4.5-d:
js-cookie "^2.1.0"
pym.js "^1.3.1"
phraseanet-production-client@0.34.160-d:
version "0.34.160-d"
resolved "https://registry.yarnpkg.com/phraseanet-production-client/-/phraseanet-production-client-0.34.160-d.tgz#3deb3387b54e56aec73b073cae8cc013cc316999"
integrity sha512-00jnCOCDrLowL8TE+h8mH+Lg4P0+EQPSDOXN2Lq6KF577GGu0OWQRh2YmUeXkacHTUT4N0u39IsBkkBYqorVnQ==
phraseanet-production-client@0.34.162-d:
version "0.34.162-d"
resolved "https://registry.yarnpkg.com/phraseanet-production-client/-/phraseanet-production-client-0.34.162-d.tgz#4bfbb6998bae864f5be2eefba87b07ef06f7e4e6"
integrity sha512-FVXzj0Qi6DQSJnv3LrCiIaEUZ0A1DoOn3kUGPBpgws+EzXS13tFC6W0eLz/gDgw/L9Y82S60gI9s778fadwoew==
dependencies:
"@mapbox/mapbox-gl-language" "^0.9.2"
"@turf/turf" "^5.1.6"