Skip to content
Snippets Groups Projects
Commit 2d0db583 authored by Alex Pott's avatar Alex Pott Committed by Daniel Bosen
Browse files

Issue #3359054 by alexpott, daniel.bosen: Add the ability to alter the VG Wort...

Issue #3359054 by alexpott, daniel.bosen: Add the ability to alter the VG Wort counter ID for an entity
parent d2e38f25
No related branches found
No related tags found
1 merge request!25Resolve #3359054 "Add the ability"
......@@ -39,6 +39,12 @@ If you need to alter the information sent to VGWort you can implement
legal flags for an entity. This is useful for decoupled sites. See
[vgwort.api.php](vgwort.api.php) for more information.
## Alter the VG Wort counter ID for an entity
This hook can be used to override the VG Wort counter ID value. By default,
the entity's UUID is used, however if you've migrated the content and wish to
preserve the ID this hook can be used to provide an alternative field name
from which to derive the value.
## Using an entity reference field to list participants
The module provides the ability to use any entity reference field to determine
VG Wort participant info. For example, if you have an author node type and
......
......@@ -101,10 +101,63 @@ final class EntityJobMapper {
*/
public function addJobToMap(Job $job, string $counter_id) {
[$entity_type, $entity_id] = RegistrationNotification::getEntityInfoFromJob($job);
$this->connection->merge(self::TABLE)
->keys(['entity_type' => $entity_type, 'entity_id' => $entity_id, 'counter_id' => $counter_id])
->fields(['job_id' => $job->getId()])
// Use a transaction to keep everything consistent. It will be committed as
// soon as $transaction is out of scope.
$transaction = $this->connection->startTransaction();
// Find rows for the entity that are still to be processed by the queue.
// If they are rows:
// - Remove the queue item if possible.
// - Delete them from the map.
// The following code is optimised to do this in the minimum queries and
// amount of logic possible as this is run as part of saving an entity.
$result = $this->connection->select(self::TABLE, 'map')
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->isNull('success_timestamp')
->fields('map', ['job_id', 'counter_id'])
->execute();
$queue_backend = $do_delete = FALSE;
foreach ($result as $row) {
if (!$do_delete) {
$do_delete = TRUE;
$queue = Queue::load('vgwort');
$queue_backend = $queue->getBackend();
}
// Intentional loose comparison because database return types are not
// trustworthy. If the passed in job matches job in the map do not delete
// the job. This is a check for robustness and safety and should never
// occur.
if ($row->job_id != $job->getId() && $queue_backend instanceof SupportsDeletingJobsInterface) {
$queue_backend->deleteJob($row->job_id);
}
}
if ($do_delete) {
// Remove all unprocessed rows for the entity.
$this->connection->delete(self::TABLE)
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->isNull('success_timestamp')
->execute();
}
// Insert a row into the map for the entity.
$this->connection->insert(self::TABLE)
->fields([
'entity_type' => $entity_type,
'entity_id' => $entity_id,
'counter_id' => $counter_id,
'job_id' => $job->getId(),
])
->execute();
if ($transaction) {
// Commit the transaction.
$transaction = NULL;
}
return $this;
}
......@@ -171,6 +224,7 @@ final class EntityJobMapper {
->condition('entity_type', $entity->getEntityTypeId())
->condition('entity_id', $entity->id())
->isNotNull('success_timestamp')
->orderBy('revision_id')
->execute();
$revisions = [];
foreach ($result as $row) {
......
......@@ -2,6 +2,7 @@
namespace Drupal\vgwort\Plugin\Field;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
......@@ -43,9 +44,40 @@ class CounterIdFieldItemList extends FieldItemList {
return;
}
// @todo Do we need to use base64 because the UUID contains hyphens?
// Example: vgzm.970-123456789
$value = "$prefix.$publisher_id-{$entity->uuid()}";
$override_counter_field_value = NULL;
if ($entity instanceof FieldableEntityInterface) {
$override_counter_field = NULL;
\Drupal::moduleHandler()->invokeAllWith('vgwort_entity_counter_id_field', function (callable $hook) use ($entity, &$override_counter_field) {
// Once an implementation has returned a value do not call any other
// implementation.
if ($override_counter_field === NULL) {
$override_counter_field = $hook($entity);
}
});
// Ensure the value returned is valid.
if ($override_counter_field !== NULL) {
if ($entity->hasField($override_counter_field)) {
$override_counter_field_value = (string) $entity->get($override_counter_field)->value;
$log_error = strlen($override_counter_field_value) === 0;
}
else {
$log_error = TRUE;
}
// If the hook implementation has returned a bogus value, ignore it and
// use the UUID instead.
if ($log_error) {
\Drupal::logger('vgwort')->error(
'An implementation of hook_vgwort_entity_counter_id_field() has returned %field_name which is not valid for entity @entity_type:@entity_id',
['%field_name' => $override_counter_field, '@entity_type' => $entity->getEntityTypeId(), '@entity_id' => $entity->id()]
);
$override_counter_field_value = NULL;
}
}
}
$value = "$prefix.$publisher_id-";
$value .= $override_counter_field_value ?? $entity->uuid();
/** @var \Drupal\Core\Field\FieldItemInterface $item */
$item = $this->createItem(0, $value);
......
......@@ -6,6 +6,7 @@
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
use Drupal\vgwort\Api\Webrange;
......@@ -42,3 +43,11 @@ function vgwort_test_vgwort_new_message_alter(array &$data, EntityInterface $ent
$data['legal_rights']['other_public_communication'] = TRUE;
}
}
/**
* Implements hook_vgwort_new_message_alter().
*/
function vgwort_test_vgwort_entity_counter_id_field(EntityInterface $entity): ?string {
assert($entity instanceof FieldableEntityInterface, 'The entity is fieldable');
return \Drupal::state()->get('vgwort_test_vgwort_entity_counter_id_field');
}
......@@ -14,6 +14,7 @@ use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use Symfony\Component\ErrorHandler\BufferingLogger;
/**
* Tests the vgwort entity queue.
......@@ -30,6 +31,11 @@ class EntityQueueTest extends VgWortKernelTestBase {
*/
private MockHandler $handler;
/**
* @var \Symfony\Component\ErrorHandler\BufferingLogger
*/
private BufferingLogger $testLogger;
/**
* {@inheritdoc}
*
......@@ -66,6 +72,11 @@ class EntityQueueTest extends VgWortKernelTestBase {
$client = new Client(['handler' => $handler_stack]);
$container->set('http_client', $client);
$container->getDefinition('datetime.time')->setClass(TimePatcher::class);
$container->register('vgwort_test.logger', BufferingLogger::class)->addTag('logger');
if (!isset($this->testLogger)) {
$this->testLogger = new BufferingLogger();
}
$container->set('vgwort_test.logger', $this->testLogger);
}
/**
......@@ -131,7 +142,7 @@ class EntityQueueTest extends VgWortKernelTestBase {
$this->assertSame(Job::STATE_QUEUED, $job->getState());
$this->assertSame('2', $job->getId());
// Test that saving the entity does add another job.
// Test that saving the entity does not add another job.
$another_entity->set('text', 'Edited text')->save();
$jobs[Job::STATE_QUEUED] = '2';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
......@@ -141,6 +152,139 @@ class EntityQueueTest extends VgWortKernelTestBase {
$this->assertGreaterThanOrEqual($this->container->get('datetime.time')->getRequestTime() + (14 * 24 * 60 * 60), $job->getAvailableTime());
}
public function testChangingVgWortCounterId() {
// Process queue immediately.
$this->config('vgwort.settings')->set('registration_wait_days', 0)->save();
$queue = Queue::load('vgwort');
$this->assertInstanceOf(Queue::class, $queue);
$jobs = self::JOB_COUNT;
$entity_storage = $this->container->get('entity_type.manager')->getStorage(static::ENTITY_TYPE);
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $entity_storage->create([
'vgwort_test' => [
'card_number' => '123123123',
'firstname' => 'Bob',
'surname' => 'Jones',
'agency_abbr' => '',
],
'text' => 'Some text',
'name' => 'A title',
]);
$entity->save();
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $another_entity */
$another_entity = $entity_storage->create([
'vgwort_test' => [
'card_number' => '123123123',
'firstname' => 'Bob',
'surname' => 'Jones',
'agency_abbr' => '',
],
'text' => 'Some text',
'name' => 'A title',
]);
$another_entity->save();
$jobs[Job::STATE_QUEUED] = '2';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
// Mark $entity as successfully sent to VG Wort.
$this->handler->append(new Response());
$this->handler->append(new Response(500, ['Content-Type' => ['application/json', 'charset=UTF-8']], '{"message":{"errorcode":1,"errormsg":"Privater Identifikationscode: Für den eingegebenen Wert existiert keine Zählmarke."}}'));
$this->container->get('advancedqueue.processor')->processQueue($queue);
$jobs = self::JOB_COUNT;
$jobs[Job::STATE_FAILURE] = '1';
$jobs[Job::STATE_SUCCESS] = '1';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
// Test changing the VG Wort Counter ID field.
\Drupal::state()->set('vgwort_test_vgwort_entity_counter_id_field', 'id');
// Reload the entities to update the computed field.
$storage = $this->container->get('entity_type.manager')->getStorage(self::ENTITY_TYPE);
$storage->resetCache();
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $storage->load($entity->id());
$this->assertSame('vgzm.123456-1', $entity->vgwort_counter_id->value);
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $another_entity */
$another_entity = $storage->load($another_entity->id());
$this->assertSame('vgzm.123456-2', $another_entity->vgwort_counter_id->value);
// Having a new counter ID will cause a new row to be added to the map and
// the old still to be processed row to be removed.
$another_entity->set('text', 'Edited text 2')->save();
$jobs = self::JOB_COUNT;
$jobs[Job::STATE_QUEUED] = '1';
$jobs[Job::STATE_SUCCESS] = '1';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
// Re-saving a successfully set entity will add a new row to be processed
// because the counter ID has changed.
$entity->setNewRevision();
$entity->set('text', 'Edited text again')->save();
$jobs[Job::STATE_QUEUED] = '2';
$jobs[Job::STATE_SUCCESS] = '1';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
$this->assertSame([1 => 'vgzm.123456-' . $entity->uuid()], $this->container->get('vgwort.entity_job_mapper')->getRevisionsSent($entity));
$this->assertSame([], $this->container->get('vgwort.entity_job_mapper')->getRevisionsSent($another_entity));
// Mark all as successfully sent to VG Wort.
$this->handler->append(new Response());
$this->handler->append(new Response());
$this->container->get('advancedqueue.processor')->processQueue($queue);
$jobs = self::JOB_COUNT;
$jobs[Job::STATE_SUCCESS] = '3';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
$this->assertSame([1 => 'vgzm.123456-' . $entity->uuid(), 3 => 'vgzm.123456-1'], $this->container->get('vgwort.entity_job_mapper')->getRevisionsSent($entity));
$this->assertSame([2 => 'vgzm.123456-2'], $this->container->get('vgwort.entity_job_mapper')->getRevisionsSent($another_entity));
}
public function testBadVgwortEntityCounterIdFieldHook() {
// Test returning a bogus value
\Drupal::state()->set('vgwort_test_vgwort_entity_counter_id_field', 'does_not_exist');
$entity_storage = $this->container->get('entity_type.manager')->getStorage(static::ENTITY_TYPE);
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $entity_storage->create([
'text' => '',
'name' => 'A title',
]);
$entity->save();
$this->assertSame('vgzm.123456-' . $entity->uuid(), $entity->vgwort_counter_id->value);
$logs = $this->testLogger->cleanLogs();
$log_entry = end($logs);
$this->assertSame($log_entry[1], "An implementation of hook_vgwort_entity_counter_id_field() has returned %field_name which is not valid for entity @entity_type:@entity_id");
$this->assertSame($log_entry[2]['%field_name'], "does_not_exist");
$this->assertSame($log_entry[2]['@entity_type'], static::ENTITY_TYPE);
$this->assertSame($log_entry[2]['@entity_id'], $entity->id());
// Set it to an empty field.
\Drupal::state()->set('vgwort_test_vgwort_entity_counter_id_field', 'text');
// Reload the entity to update the computed field.
$storage = $this->container->get('entity_type.manager')->getStorage(self::ENTITY_TYPE);
$storage->resetCache();
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $storage->load($entity->id());
$this->assertSame('vgzm.123456-' . $entity->uuid(), $entity->vgwort_counter_id->value);
$logs = $this->testLogger->cleanLogs();
$log_entry = end($logs);
$this->assertSame($log_entry[1], "An implementation of hook_vgwort_entity_counter_id_field() has returned %field_name which is not valid for entity @entity_type:@entity_id");
$this->assertSame($log_entry[2]['%field_name'], "text");
$this->assertSame($log_entry[2]['@entity_type'], static::ENTITY_TYPE);
$this->assertSame($log_entry[2]['@entity_id'], $entity->id());
// Finally test a valid implementation.
$entity->setNewRevision();
$entity->set('text', 'test')->save();
$storage = $this->container->get('entity_type.manager')->getStorage(self::ENTITY_TYPE);
$storage->resetCache();
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $storage->load($entity->id());
$this->assertSame('vgzm.123456-test', $entity->vgwort_counter_id->value);
$this->assertSame([], $this->testLogger->cleanLogs());
}
public function testQueueProcessing() {
// Process queue immediately.
$this->config('vgwort.settings')->set('registration_wait_days', 0)->save();
......@@ -365,4 +509,13 @@ class EntityQueueTest extends VgWortKernelTestBase {
->fetchField();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
parent::tearDown();
// Ensure all the logs are cleaned up.
$this->testLogger->cleanLogs();
}
}
......@@ -23,6 +23,34 @@ function hook_vgwort_enable_for_entity(\Drupal\Core\Entity\EntityInterface $enti
return $entity->bundle() !== 'not_my_content';
}
/**
* Overrides the field used to determine the VG Wort counter ID.
*
* This hook can be used to override the VG Wort counter ID value. By default,
* the entity's UUID is used, however if you've migrated the content and wish to
* preserve the ID this hook can be used to provide an alternative field name
* from which to derive the value.
*
* NOTE: It is the hook's responsibility to ensure that the entity has a value
* for the field that is being returned. If the field does not exist or results
* in a zero length string the UUID will be used instead and this will be
* logged.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to determine the field for.
*
* @return string|null
* The field to use for the VG Wort counter ID. Return NULL to leave as UUID
* or be determined by another implementation. First hook to return a value
* wins.
*/
function hook_vgwort_entity_counter_id_field(\Drupal\Core\Entity\EntityInterface $entity): ?string {
if ($entity->bundle() === 'not_my_content' && $entity instanceof \Drupal\Core\Entity\FieldableEntityInterface && $entity->hasField('field_my_special_id')) {
return 'field_my_special_id';
}
return NULL;
}
/**
* Alters the information sent about an entity to VG Wort.
*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment