
18 changed files with 666 additions and 348 deletions
@ -0,0 +1,254 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
use Drupal\Core\Database\Connection; |
||||||
|
use Drupal\Core\DependencyInjection\DependencySerializationTrait; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Messenger\MessengerInterface; |
||||||
|
use Drupal\Core\Session\AccountProxyInterface; |
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait; |
||||||
|
use Drupal\Core\Url; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\islandora\IslandoraUtils; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException; |
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Children addition batch processor. |
||||||
|
*/ |
||||||
|
abstract class AbstractBatchProcessor { |
||||||
|
|
||||||
|
use FieldTrait; |
||||||
|
use DependencySerializationTrait; |
||||||
|
use StringTranslationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity type manager service. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null |
||||||
|
*/ |
||||||
|
protected ?EntityTypeManagerInterface $entityTypeManager = NULL; |
||||||
|
|
||||||
|
/** |
||||||
|
* The database connection serivce. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Database\Connection|null |
||||||
|
*/ |
||||||
|
protected ?Connection $database; |
||||||
|
|
||||||
|
/** |
||||||
|
* The current user. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Session\AccountProxyInterface|null |
||||||
|
*/ |
||||||
|
protected ?AccountProxyInterface $currentUser; |
||||||
|
|
||||||
|
/** |
||||||
|
* The messenger service. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Messenger\MessengerInterface |
||||||
|
*/ |
||||||
|
protected MessengerInterface $messenger; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor. |
||||||
|
*/ |
||||||
|
public function __construct( |
||||||
|
EntityTypeManagerInterface $entity_type_manager, |
||||||
|
Connection $database, |
||||||
|
AccountProxyInterface $current_user, |
||||||
|
MessengerInterface $messenger |
||||||
|
) { |
||||||
|
$this->entityTypeManager = $entity_type_manager; |
||||||
|
$this->database = $database; |
||||||
|
$this->currentUser = $current_user; |
||||||
|
$this->messenger = $messenger; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements callback_batch_operation() for our child addition batch. |
||||||
|
*/ |
||||||
|
public function batchOperation($delta, $info, array $values, &$context) { |
||||||
|
$transaction = $this->database->startTransaction(); |
||||||
|
|
||||||
|
try { |
||||||
|
$entities[] = $this->persistFile($info, $values); |
||||||
|
$entities[] = $node = $this->getNode($info, $values); |
||||||
|
$entities[] = $this->createMedia($node, $info, $values); |
||||||
|
|
||||||
|
$context['results'] = array_merge_recursive($context['results'], [ |
||||||
|
'validation_violations' => $this->validationClassification($entities), |
||||||
|
]); |
||||||
|
$context['results']['count'] += 1; |
||||||
|
} |
||||||
|
catch (HttpExceptionInterface $e) { |
||||||
|
$transaction->rollBack(); |
||||||
|
throw $e; |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
$transaction->rollBack(); |
||||||
|
throw new HttpException(500, $e->getMessage(), $e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Loads the file indicated. |
||||||
|
* |
||||||
|
* @param array $info |
||||||
|
* An associative array containing at least: |
||||||
|
* - target_id: The id of the file to load. |
||||||
|
* |
||||||
|
* @return \Drupal\file\FileInterface |
||||||
|
* The loaded file. |
||||||
|
* |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException |
||||||
|
*/ |
||||||
|
protected function getFile(array $info) : FileInterface { |
||||||
|
return $this->entityTypeManager->getStorage('file')->load($info['target_id']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Loads and marks the target file as permanent. |
||||||
|
* |
||||||
|
* @param array $info |
||||||
|
* An associative array containing at least: |
||||||
|
* - target_id: The id of the file to load. |
||||||
|
* |
||||||
|
* @return \Drupal\file\FileInterface |
||||||
|
* The loaded file, after it has been marked as permanent. |
||||||
|
* |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException |
||||||
|
* @throws \Drupal\Core\Entity\EntityStorageException |
||||||
|
*/ |
||||||
|
protected function persistFile(array $info) : FileInterface { |
||||||
|
$file = $this->getFile($info); |
||||||
|
$file->setPermanent(); |
||||||
|
if ($file->save() !== SAVED_UPDATED) { |
||||||
|
throw new \Exception("Failed to update file '{$file->id()}' to be permanent."); |
||||||
|
} |
||||||
|
|
||||||
|
return $file; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the node to which to attach our media. |
||||||
|
* |
||||||
|
* @param array $info |
||||||
|
* Info from the widget used to create the request. |
||||||
|
* @param array $values |
||||||
|
* Additional form inputs. |
||||||
|
* |
||||||
|
* @return \Drupal\node\NodeInterface |
||||||
|
* The node to which to attach the created media. |
||||||
|
*/ |
||||||
|
abstract protected function getNode(array $info, array $values) : NodeInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a media referencing the given file, associated with the given node. |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The node to which the media should be associated. |
||||||
|
* @param array $info |
||||||
|
* The widget info, which should have a 'target_id' identifying the target |
||||||
|
* file. |
||||||
|
* @param array $values |
||||||
|
* Values from the wizard, which should contain at least: |
||||||
|
* - media_type: The machine name/ID of the media type as which to create |
||||||
|
* the media |
||||||
|
* - use: An array of the selected "media use" terms. |
||||||
|
* |
||||||
|
* @return \Drupal\media\MediaInterface |
||||||
|
* The created media entity. |
||||||
|
* |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException |
||||||
|
* @throws \Drupal\Core\Entity\EntityStorageException |
||||||
|
*/ |
||||||
|
protected function createMedia(NodeInterface $node, array $info, array $values) : MediaInterface { |
||||||
|
$taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); |
||||||
|
|
||||||
|
$file = $this->getFile($info); |
||||||
|
|
||||||
|
// Create a media with the file attached and also pointing at the node. |
||||||
|
$field = $this->getField($values); |
||||||
|
|
||||||
|
$media_values = array_merge( |
||||||
|
[ |
||||||
|
'bundle' => $values['media_type'], |
||||||
|
'name' => $file->getFilename(), |
||||||
|
IslandoraUtils::MEDIA_OF_FIELD => $node, |
||||||
|
IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? |
||||||
|
$taxonomy_term_storage->loadMultiple($values['use']) : |
||||||
|
NULL), |
||||||
|
'uid' => $this->currentUser->id(), |
||||||
|
// XXX: Published... no constant? |
||||||
|
'status' => 1, |
||||||
|
], |
||||||
|
[ |
||||||
|
$field->getName() => [ |
||||||
|
$info, |
||||||
|
], |
||||||
|
] |
||||||
|
); |
||||||
|
$media = $this->entityTypeManager->getStorage('media')->create($media_values); |
||||||
|
if ($media->save() !== SAVED_NEW) { |
||||||
|
throw new \Exception("Failed to create media for file '{$file->id()}."); |
||||||
|
} |
||||||
|
|
||||||
|
return $media; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper to bulk process validatable entities. |
||||||
|
* |
||||||
|
* @param array $entities |
||||||
|
* An array of entities to scan for validation violations. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* An associative array mapping entity type IDs to entity IDs to a count |
||||||
|
* of validation violations found on then given entity. |
||||||
|
*/ |
||||||
|
protected function validationClassification(array $entities) { |
||||||
|
$violations = []; |
||||||
|
|
||||||
|
foreach ($entities as $entity) { |
||||||
|
$entity_violations = $entity->validate(); |
||||||
|
if ($entity_violations->count() > 0) { |
||||||
|
$violations[$entity->getEntityTypeId()][$entity->id()] = $entity_violations->count(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $violations; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements callback_batch_finished() for our child addition batch. |
||||||
|
*/ |
||||||
|
public function batchProcessFinished($success, $results, $operations): void { |
||||||
|
if ($success) { |
||||||
|
foreach ($results['validation_violations'] ?? [] as $entity_type => $info) { |
||||||
|
foreach ($info as $id => $count) { |
||||||
|
$this->messenger->addWarning($this->formatPlural( |
||||||
|
$count, |
||||||
|
'1 validation error present in <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.', |
||||||
|
'@count validation errors present in <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.', |
||||||
|
[ |
||||||
|
'%type' => $entity_type, |
||||||
|
':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(), |
||||||
|
'%id' => $id, |
||||||
|
] |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->messenger->addError($this->t('Encountered an error when processing.')); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\Core\Url; |
||||||
|
|
||||||
|
/** |
||||||
|
* Children addition wizard's second step. |
||||||
|
*/ |
||||||
|
class ChildFileSelectionForm extends AbstractFileSelectionForm { |
||||||
|
|
||||||
|
public const BATCH_PROCESSOR = 'islandora.upload_children.batch_processor'; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFormId() { |
||||||
|
return 'islandora_add_children_wizard_file_selection'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||||
|
parent::submitForm($form, $form_state); |
||||||
|
|
||||||
|
$cached_values = $form_state->getTemporaryValue('wizard'); |
||||||
|
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
/** |
||||||
|
* Bulk children addition wizard base form. |
||||||
|
*/ |
||||||
|
class ChildForm extends AbstractForm { |
||||||
|
|
||||||
|
const TEMPSTORE_ID = 'islandora.upload_children'; |
||||||
|
const TYPE_SELECTION_FORM = ChildTypeSelectionForm::class; |
||||||
|
const FILE_SELECTION_FORM = ChildFileSelectionForm::class; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getMachineName() { |
||||||
|
return strtr("islandora_add_children_wizard__{userid}__{nodeid}", [ |
||||||
|
'{userid}' => $this->currentUser->id(), |
||||||
|
'{nodeid}' => $this->nodeId, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\CacheableMetadata; |
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\islandora\IslandoraUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Children addition wizard's first step. |
||||||
|
*/ |
||||||
|
class ChildTypeSelectionForm extends MediaTypeSelectionForm { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFormId() { |
||||||
|
return 'islandora_add_children_type_selection'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Memoization for ::getNodeBundleOptions(). |
||||||
|
* |
||||||
|
* @var array|null |
||||||
|
*/ |
||||||
|
protected ?array $nodeBundleOptions = NULL; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicate presence of model field on node bundles. |
||||||
|
* |
||||||
|
* Populated as a side effect of ::getNodeBundleOptions(). |
||||||
|
* |
||||||
|
* @var array|null |
||||||
|
*/ |
||||||
|
protected ?array $nodeBundleHasModelField = NULL; |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper; get the node bundle options available to the current user. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* An associative array mapping node bundle machine names to their human- |
||||||
|
* readable labels. |
||||||
|
*/ |
||||||
|
protected function getNodeBundleOptions() : array { |
||||||
|
if ($this->nodeBundleOptions === NULL) { |
||||||
|
$this->nodeBundleOptions = []; |
||||||
|
$this->nodeBundleHasModelField = []; |
||||||
|
|
||||||
|
$access_handler = $this->entityTypeManager->getAccessControlHandler('node'); |
||||||
|
foreach ($this->entityTypeBundleInfo->getBundleInfo('node') as $bundle => $info) { |
||||||
|
$access = $access_handler->createAccess( |
||||||
|
$bundle, |
||||||
|
NULL, |
||||||
|
[], |
||||||
|
TRUE |
||||||
|
); |
||||||
|
$this->cacheableMetadata->addCacheableDependency($access); |
||||||
|
if (!$access->isAllowed()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$this->nodeBundleOptions[$bundle] = $info['label']; |
||||||
|
$fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle); |
||||||
|
$this->nodeBundleHasModelField[$bundle] = array_key_exists(IslandoraUtils::MODEL_FIELD, $fields); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $this->nodeBundleOptions; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a mapping of taxonomy term IDs to their names. |
||||||
|
* |
||||||
|
* @return \Generator |
||||||
|
* The mapping of taxonomy term IDs to their names. |
||||||
|
* |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException |
||||||
|
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException |
||||||
|
*/ |
||||||
|
protected function getModelOptions() : \Generator { |
||||||
|
$terms = $this->entityTypeManager->getStorage('taxonomy_term') |
||||||
|
->loadTree('islandora_models', 0, NULL, TRUE); |
||||||
|
foreach ($terms as $term) { |
||||||
|
yield $term->id() => $term->getName(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper; map node bundles supporting the "has model" field, for #states. |
||||||
|
* |
||||||
|
* @return \Generator |
||||||
|
* Yields associative array mapping the string 'value' to the bundles which |
||||||
|
* have the given field. |
||||||
|
*/ |
||||||
|
protected function mapModelStates() : \Generator { |
||||||
|
$this->getNodeBundleOptions(); |
||||||
|
foreach (array_keys(array_filter($this->nodeBundleHasModelField)) as $bundle) { |
||||||
|
yield ['value' => $bundle]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function buildForm(array $form, FormStateInterface $form_state) { |
||||||
|
$this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form) |
||||||
|
->addCacheContexts([ |
||||||
|
'url', |
||||||
|
'url.query_args', |
||||||
|
]); |
||||||
|
$cached_values = $form_state->getTemporaryValue('wizard'); |
||||||
|
|
||||||
|
$form['bundle'] = [ |
||||||
|
'#type' => 'select', |
||||||
|
'#title' => $this->t('Content Type'), |
||||||
|
'#description' => $this->t('Each child created will have this content type.'), |
||||||
|
'#empty_value' => '', |
||||||
|
'#default_value' => $cached_values['bundle'] ?? '', |
||||||
|
'#options' => $this->getNodeBundleOptions(), |
||||||
|
'#required' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$model_states = iterator_to_array($this->mapModelStates()); |
||||||
|
$form['model'] = [ |
||||||
|
'#type' => 'select', |
||||||
|
'#title' => $this->t('Model'), |
||||||
|
'#description' => $this->t('Each child will be tagged with this model.'), |
||||||
|
'#options' => iterator_to_array($this->getModelOptions()), |
||||||
|
'#empty_value' => '', |
||||||
|
'#default_value' => $cached_values['model'] ?? '', |
||||||
|
'#states' => [ |
||||||
|
'visible' => [ |
||||||
|
':input[name="bundle"]' => $model_states, |
||||||
|
], |
||||||
|
'required' => [ |
||||||
|
':input[name="bundle"]' => $model_states, |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
$this->cacheableMetadata->applyTo($form); |
||||||
|
return parent::buildForm($form, $form_state); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected static function keysToSave() { |
||||||
|
return array_merge( |
||||||
|
parent::keysToSave(), |
||||||
|
[ |
||||||
|
'bundle', |
||||||
|
'model', |
||||||
|
] |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Media addition batch processor. |
||||||
|
*/ |
||||||
|
class MediaBatchProcessor extends AbstractBatchProcessor { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function getNode(array $info, array $values) : NodeInterface { |
||||||
|
return $this->entityTypeManager->getStorage('node')->load($values['node']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function batchProcessFinished($success, $results, $operations): void { |
||||||
|
if ($success) { |
||||||
|
$this->messenger->addMessage($this->formatPlural( |
||||||
|
$results['count'], |
||||||
|
'Added 1 media.', |
||||||
|
'Added @count media.' |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
parent::batchProcessFinished($success, $results, $operations); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\Core\Url; |
||||||
|
|
||||||
|
/** |
||||||
|
* Media addition wizard's second step. |
||||||
|
*/ |
||||||
|
class MediaFileSelectionForm extends AbstractFileSelectionForm { |
||||||
|
|
||||||
|
public const BATCH_PROCESSOR = 'islandora.upload_media.batch_processor'; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFormId() { |
||||||
|
return 'islandora_add_media_wizard_file_selection'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||||
|
parent::submitForm($form, $form_state); |
||||||
|
|
||||||
|
$cached_values = $form_state->getTemporaryValue('wizard'); |
||||||
|
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/media")); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\Form\AddChildrenWizard; |
||||||
|
|
||||||
|
/** |
||||||
|
* Bulk children addition wizard base form. |
||||||
|
*/ |
||||||
|
class MediaForm extends AbstractForm { |
||||||
|
|
||||||
|
const TEMPSTORE_ID = 'islandora.upload_media'; |
||||||
|
const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class; |
||||||
|
const FILE_SELECTION_FORM = MediaFileSelectionForm::class; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getMachineName() { |
||||||
|
return strtr("islandora_add_media_wizard__{userid}__{nodeid}", [ |
||||||
|
'{userid}' => $this->currentUser->id(), |
||||||
|
'{nodeid}' => $this->nodeId, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue