15 Commits
v1.x ... v2.x

Author SHA1 Message Date
jbcr
657c269eb3 add log in content writer and notmodifiercontentfilter (#35)
* add log in content writer and notmodifiercontentfilter

* add changelog and readme
2021-01-15 17:14:07 +01:00
jbcr
6f1a719314 Update CHANGELOG.md 2021-01-14 17:07:55 +01:00
jeremycr
52f607616d Added a button to display exceptions / log in a modal (#33) 2021-01-14 17:06:41 +01:00
jeremycr
0c13878f79 Added content fields comparison to ignore updates that would result in no change (#26) 2020-10-23 10:23:05 +02:00
flovntp
8b29a92aab Update CHANGELOG.md 2020-06-03 14:57:13 +02:00
flovntp
c824c0ecbb Update ContentWriter.php (#31) 2020-06-03 14:18:12 +02:00
Nicolas Lœuillet
760aea3948 Fixed 🇫🇷 translation (#30) 2019-12-20 10:39:04 +01:00
jbcr
e1213d4a74 add v2.0.1 changelog (#29) 2019-12-05 16:24:18 +01:00
jbcr
5f3775cb6b enclosure js code into anonymous function (#28)
* encosure js code into annonymous function
2019-12-05 15:24:17 +01:00
jbcr
ec42d6f765 add right informations (#27)
* add right informations

* Update README.md

Co-Authored-By: jeremycr <32451794+jeremycr@users.noreply.github.com>
2019-12-05 15:11:10 +01:00
jeremycr
deab8e8073 Remove redundant code (#23) 2019-11-28 14:17:26 +01:00
jbcr
ca37b2438d Add the ContentStructureFactory::transform() Mode. (#19)
* implement #18
* add an interface for factory
2019-11-13 10:36:37 +01:00
jbcr
d166f3aea8 Update to v2 (#15)
* refs #13

* update reload JS after form sent
* add eror management in job details view
* update Job and Schedule gateway
* update view
* remove unnecessary use
* add BC doc and fix error if no result in findForScheduled
* update dev dependency
* set final some class
* fix bug: form variable access on second-page loading
* add pager for scheduled dataflow list
2019-11-12 11:00:40 +01:00
jbcr
9ed5fd3c2b update docs #16 (#17)
* update docs #16

* add text style
2019-11-08 11:19:12 +01:00
jbcr
f98fecdca9 refs #12 (#14) 2019-11-08 11:18:54 +01:00
38 changed files with 1222 additions and 380 deletions

31
CHANGELOG.md Normal file
View File

@@ -0,0 +1,31 @@
# Version 2.3.0
* Added a button to display exceptions / log in a modal
* Add log in `CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter` and `CodeRhapsodie\EzDataflowBundle\Writer\ContentWriter`
# Version 2.2.0
* Added `NotModifiedContentFilter` and a bunch of `FieldComparator` classes
# version 2.1.0
* ContentWriter return created content
# version 2.0.1
* Enclosure js code into anonymous function
# version 2.0.0
* Update to use Dataflow v2.0+
* Add compiler pass to change the Dataflow DBAL connection factory
* Use the DBAL connection from siteaccess
* Add `mode` argument on `ContentStructureFactory::transform()` function
* Add `CodeRhapsodie\EzDataflowBundle\Factory\ContentStructureFactoryInterface`
# version 1.0.0
* Initial version to use Dataflow v1.0+ into eZ Platform
* Add Admin UI
* Add content writer
* Add content structure

View File

@@ -6,6 +6,8 @@ EzDataflow bundle is intended to manage content imports from external data sourc
> Note: before using this bundle, please read the [Code Rhapsodie Dataflow bundle documentation](https://github.com/code-rhapsodie/dataflow-bundle/blob/master/README.md).
> Command line notice: When you use Dataflow commands, **use `--siteaccess` instead of `--connection`** expect for `code-rhapsodie:dataflow:dump-schema` command.
## User Interface (UI)
The UI lets you create workflow processes from any defined `DataflowTypes`, and set options to each.
@@ -24,6 +26,8 @@ $ composer require code-rhapsodie/ezdataflow-bundle
### Step 2: Enable the bundle
> Note: The loading order between the Dataflow bundle and Ez Dataflow bundle is important. Dataflow must be loaded first.
#### Symfony 4 (new tree)
For Symfony 4, add those two lines in the `config/bundles.php` file:
@@ -174,7 +178,7 @@ class MyDataflowType extends AbstractDataflowType
/**
* @var ContentStructureFactory
*/
private contentStructureFactory;
private $contentStructureFactory;
public function __construct(ContentWriter $contentWriter, ContentStructureFactory $contentStructureFactory)
{
@@ -198,9 +202,12 @@ class MyDataflowType extends AbstractDataflowType
$remoteId,
'eng-GB',
'article2',
54 //Parent location id
54, //Parent location id
ContentStructureFactoryInterface::MODE_INSERT_OR_UPDATE //Optional value. Other choice : ContentStructureFactoryInterface::MODE_INSERT_ONLY or ContentStructureFactoryInterface::MODE_UPDATE_ONLY
);
});
// If you want the writer log
$this->contentWriter->setLogger($this->logger);
$builder->addWriter($this->contentWriter);
}
}
@@ -208,6 +215,85 @@ class MyDataflowType extends AbstractDataflowType
This example uses `ContentStructureFactory` to check if the content exists and returns the adequate `ContentStrucure` to pass to the content writer.
## Use the NotModifiedContentFilter
When updating contents, you might want to ignore contents where the update would not result in any actual changes in fields values. In that case, you can add the `NotModifiedContentFilter` as one of your steps.
```php
// In your DataflowType
public function __construct(NotModifiedContentFilter $notModifiedContentFilter)
{
$this->notModifiedContentFilter = $notModifiedContentFilter;
}
//[...]
protected function buildDataflow(DataflowBuilder $builder, array $options): void
{
//[...]
// If you want the filter log
$this->notModifiedContentFilter->setLogger($this->logger);
$builder->addStep($this->notModifiedContentFilter);
//[...]
}
```
This filter compares each field value in the `ContentUpdateStructure` received to the fields values in the existing content object. If all values are identical, this filter will return `false`, otherwise, the `ContentUpdateStructure` will be returned as is.
Not all field types are supported by this filter. Il a field type is not supported, values will be assumed different. If your dataflow is dealing with content types containing unsupported field types, it is better to simply not use the `NotModifiedContentFilter` to prevent unnecessary overhead.
### Supported field types
- ezstring
- ezauthor
- ezboolean
- ezcountry
- ezdate
- ezdatetime
- ezemail
- ezfloat
- ezisbn
- ezobjectrelation
- ezobjectrelationlist
- ezkeyword
- ezselection
- eztext
- eztime
- eztags
- novaseometas
- ezurl
- ezmatrix
- ezgmaplocation
- ezrichtext
### Add custom field comparator
If you want to add support for a field type, simply create your own comparator.
```php
<?php
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator;
use eZ\Publish\Core\FieldType\Value;
//[...]
class MyFieldComparator extends AbstractFieldComparator
{
//[...]
protected function compareValues(Value $currentValue, Value $newValue): bool
{
// Return true if values are identical, false if values are different.
}
}
```
```yaml
# Service declaration
App\FieldComparator\MyFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'my_field_type_identifier' }
```
# Admin UI
## Access to the eZ Dataflow UI
@@ -297,6 +383,10 @@ Go to the eZ Dataflow admin UI and click on the "Oneshot" tab. Click on the "+"
Finally, click on the "Create" button.
# Rights
If a non-administrator user needs read-only access to the dataflow interface, add the `Setup / Administrate` and `eZ Dataflow / View` policies in one of their roles.
# Issues and feature requests
Please report issues and request features at https://github.com/code-rhapsodie/ezdataflow-bundle/issues.
@@ -310,4 +400,3 @@ already.
# License
This package is licensed under the [MIT license](LICENSE).

15
UPGRADE.md Normal file
View File

@@ -0,0 +1,15 @@
# From v1.0 to v2.0
When you use Dataflow commands, use `--siteaccess` instead of `--connection` except for `code-rhapsodie:dataflow:dump-schema`.
[BC] The return of `CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway::findForScheduled`
and `CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway::findAllOrderedByLabel` has been changed.
The iterable contains an associative array instead of an object.
[BC] In classes `CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway` and
`CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway`, all methods return `Doctrine\ORM\Query` object has
changed to return now a `Doctrine\DBAL\Query\QueryBuilder`
[BC] The return type of `CodeRhapsodie\EzDataflowBundle\Factory\ContentStructureFactory::transform` has been changed
from `CodeRhapsodie\EzDataflowBundle\Model\ContentStructure` to `mixed`. In fact only `false` or
`CodeRhapsodie\EzDataflowBundle\Model\ContentStructure` object will be returned.

View File

@@ -41,13 +41,14 @@
}
},
"require": {
"code-rhapsodie/dataflow-bundle": "^1.0 || dev-master",
"php": "^7.1",
"code-rhapsodie/dataflow-bundle": "^2.1 || dev-master",
"ezsystems/ezplatform-admin-ui": "^1.0",
"ezsystems/ezpublish-kernel": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7||^8",
"doctrine/orm": "^2.4.5"
"doctrine/dbal": "^2.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@@ -56,7 +57,8 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-master": "2.x-dev",
"dev-v1.x": "1.x-dev"
}
}
}

View File

@@ -4,7 +4,7 @@
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="Tests/bootstrap.php"
bootstrap="tests/bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
@@ -14,16 +14,16 @@
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Port tests suite">
<directory suffix="Test.php">./Tests</directory>
<testsuite name="EzDataflow tests suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<directory>./src/</directory>
<exclude>
<directory>Tests/</directory>
<directory>tests/</directory>
<directory>vendor/</directory>
</exclude>
</whitelist>

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle;
use CodeRhapsodie\EzDataflowBundle\DependencyInjection\CodeRhapsodieEzDataflowExtension;
use CodeRhapsodie\EzDataflowBundle\DependencyInjection\Compiler\FieldComparatorCompilerPass;
use CodeRhapsodie\EzDataflowBundle\Security\PolicyProvider;
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\EzPublishCoreExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -23,6 +24,8 @@ class CodeRhapsodieEzDataflowBundle extends Bundle
{
parent::build($container);
$container->addCompilerPass(new FieldComparatorCompilerPass());
/** @var EzPublishCoreExtension $eZExtension */
$eZExtension = $container->getExtension('ezpublish');
$eZExtension->addPolicyProvider(new PolicyProvider());

View File

@@ -10,11 +10,11 @@ use CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use Doctrine\ORM\Query;
use Doctrine\DBAL\Query\QueryBuilder;
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
use EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface;
use EzSystems\EzPlatformAdminUiBundle\Controller\Controller;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Adapter\DoctrineDbalAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -51,7 +51,7 @@ class DashboardController extends Controller
return $this->render('@ezdesign/ezdataflow/Dashboard/main.html.twig');
}
public function repeating(): Response
public function repeating(Request $request): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
@@ -62,11 +62,27 @@ class DashboardController extends Controller
]);
return $this->render('@ezdesign/ezdataflow/Dashboard/repeating.html.twig', [
'items' => $this->scheduledDataflowGateway->findAllOrderedByLabel(),
'pager' => $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request),
'form' => $form->createView(),
]);
}
/**
* @Route("/repeating", name="coderhapsodie.ezdataflow.repeating")
*
* @param Request $request
*
* @return Response
*/
public function getRepeatingPage(Request $request): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
return $this->render('@ezdesign/ezdataflow/Dashboard/repeating.html.twig', [
'pager' => $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request),
]);
}
public function oneshot(Request $request): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
@@ -142,9 +158,12 @@ class DashboardController extends Controller
]);
}
private function getPager(Query $query, Request $request): Pagerfanta
private function getPager(QueryBuilder $query, Request $request): Pagerfanta
{
$pager = new Pagerfanta(new DoctrineORMAdapter($query));
$pager = new Pagerfanta(new DoctrineDbalAdapter($query, function ($queryBuilder) {
return $queryBuilder->select('COUNT(DISTINCT id) AS total_results')
->setMaxResults(1);
}));
$pager->setMaxPerPage(20);
$pager->setCurrentPage($request->query->get('page', 1));

View File

@@ -10,11 +10,12 @@ use CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway;
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
use EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface;
use EzSystems\EzPlatformAdminUiBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* @Route("/ezdataflow/job")
@@ -54,6 +55,26 @@ class JobController extends Controller
]);
}
/**
* @Route("/details/log/{id}", name="coderhapsodie.ezdataflow.job.log")
*
* @param int $id
*
* @return Response
*/
public function displayLog(int $id): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
$item = $this->jobGateway->find($id);
$log = array_map(function ($line) {
return preg_replace('~#\d+~', "\n$0", $line);
}, $item->getExceptions());
return $this->render('@ezdesign/ezdataflow/Item/log.html.twig', [
'log' => $log,
]);
}
/**
* @Route("/create", name="coderhapsodie.ezdataflow.job.create", methods={"POST"})
*
@@ -82,7 +103,8 @@ class JobController extends Controller
}
return new JsonResponse([
'redirect' => $this->generateUrl('coderhapsodie.ezdataflow.main', ['_fragment' => 'oneshot']),
'redirect' => $this->generateUrl('coderhapsodie.ezdataflow.main', ['_fragment' => 'oneshot'],
UrlGeneratorInterface::ABSOLUTE_URL),
]);
}

View File

@@ -13,9 +13,6 @@ class DefaultFieldValueCreator implements FieldValueCreatorInterface
/** @var FieldTypeService */
private $fieldTypeService;
/** @var FieldType[] */
private $fieldTypes = [];
public function __construct(FieldTypeService $fieldTypeService)
{
$this->fieldTypeService = $fieldTypeService;
@@ -28,22 +25,6 @@ class DefaultFieldValueCreator implements FieldValueCreatorInterface
public function createValue(string $fieldTypeIdentifier, $hash): Value
{
return $this->getFieldType($fieldTypeIdentifier)->fromHash($hash);
}
/**
* @param string $fieldTypeIdentifier
*
* @return FieldType
*
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
*/
private function getFieldType(string $fieldTypeIdentifier): FieldType
{
if (!isset($this->fieldTypes[$fieldTypeIdentifier])) {
$this->fieldTypes[$fieldTypeIdentifier] = $this->fieldTypeService->getFieldType($fieldTypeIdentifier);
}
return $this->fieldTypes[$fieldTypeIdentifier];
return $this->fieldTypeService->getFieldType($fieldTypeIdentifier)->fromHash($hash);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\FieldTypeService;
use eZ\Publish\API\Repository\Values\Content\Field;
use eZ\Publish\Core\FieldType\Value;
abstract class AbstractFieldComparator implements FieldComparatorInterface
{
/** @var FieldTypeService */
private $fieldTypeService;
public function __construct(FieldTypeService $fieldTypeService)
{
$this->fieldTypeService = $fieldTypeService;
}
public function compare(Field $field, $hash): bool
{
$newValue = $this->fieldTypeService->getFieldType($field->fieldTypeIdentifier)->fromHash($hash);
return $this->compareValues($field->value, $newValue);
}
/**
* Returns true if values are equals, false otherwise
*/
abstract protected function compareValues(Value $currentValue, Value $newValue): bool;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\Values\Content\Field;
class DelegatorFieldComparator implements FieldComparatorInterface
{
/** @var FieldComparatorInterface[] */
private $delegates;
/**
* FieldComparator constructor.
*/
public function __construct()
{
$this->delegates = [];
}
public function compare(Field $field, $hash): bool
{
if (isset($this->delegates[$field->fieldTypeIdentifier])) {
return $this->delegates[$field->fieldTypeIdentifier]->compare($field, $hash);
}
// No comparator to handle this field type, we assume the value is different.
return false;
}
public function registerDelegateFieldComparator(FieldComparatorInterface $typedFieldComparator, string $fieldTypeIdentifier): void
{
$this->delegates[$fieldTypeIdentifier] = $typedFieldComparator;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\Values\Content\Field;
interface FieldComparatorInterface
{
/**
* @return bool true if identical, false otherwise
*/
public function compare(Field $field, $hash): bool;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class MapLocationFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return (string) $currentValue === (string) $newValue
&& (float) $currentValue->longitude === (float) $newValue->longitude
&& (float) $currentValue->latitude === (float) $newValue->latitude
;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class MatrixFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
if (count($currentValue->rows) !== count($newValue->rows)) {
return false;
}
foreach ($newValue->rows as $index => $row) {
if (count($currentValue->rows[$index]->getCells()) !== count($row->getCells())) {
return false;
}
if (!empty(array_diff_assoc($currentValue->rows[$index]->getCells(), $row->getCells()))) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class NovaSEOMetasFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
$map = [];
foreach ($currentValue->metas as $meta) {
$map[$meta->getName()] = $meta->getContent();
}
foreach ($newValue->metas as $meta) {
if (!isset($map[$meta->getName()]) || $map[$meta->getName()] !== $meta->getContent()) {
return false;
}
}
return count($currentValue->metas) === count($newValue->metas);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class SimpleFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return (string) $currentValue === (string) $newValue;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class UrlFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return $currentValue->link === $newValue->link && $currentValue->text === $newValue->text;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\DependencyInjection\Compiler;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class FieldComparatorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(DelegatorFieldComparator::class)) {
return;
}
$delegatorDef = $container->findDefinition(DelegatorFieldComparator::class);
foreach ($container->findTaggedServiceIds('coderhapsodie.ezdataflow.field_comparator') as $id => $tags) {
foreach ($tags as $attributes) {
if (!isset($attributes['fieldType'])) {
throw new \InvalidArgumentException(sprintf('Service "%s" must define the "fieldType" attribute on "coderhapsodie.ezdataflow.field_comparator" tags.', $id));
}
$delegatorDef->addMethodCall(
'registerDelegateFieldComparator',
[new Reference($id), $attributes['fieldType']]
);
}
}
}
}

View File

@@ -10,7 +10,7 @@ use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
class ContentStructureFactory
final class ContentStructureFactory implements ContentStructureFactoryInterface
{
/**
* @var ContentService
@@ -28,27 +28,35 @@ class ContentStructureFactory
}
/**
* @param array $data
* @param string $remoteId
* @param string $language
* @param string $contentType
* @param mixed $parentLocations
* @param array $data
* @param string $remoteId
* @param string $language
* @param string $contentType
* @param mixed $parentLocations
* @param int $mode One of the constant ContentStructureFactoryInterface::MODE_*
*
* @return ContentStructure
* @return false|ContentStructure
*
* @throws \CodeRhapsodie\EzDataflowBundle\Exception\InvalidArgumentTypeException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
public function transform(array $data, string $remoteId, string $language, string $contentType, $parentLocations): ContentStructure
public function transform(array $data, string $remoteId, string $language, string $contentType, $parentLocations, int $mode = ContentStructureFactoryInterface::MODE_INSERT_OR_UPDATE)
{
try {
$content = $this->contentService->loadContentByRemoteId($remoteId);
if ($mode === static::MODE_INSERT_ONLY) {
return false;
}
return ContentUpdateStructure::createForContentId($content->id, $language, $data);
} catch (NotFoundException $e) {
// The content doesn't exist yet, so it will be created.
}
if ($mode === static::MODE_UPDATE_ONLY) {
return false;
}
return new ContentCreateStructure(
$contentType,
$language,

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Factory;
interface ContentStructureFactoryInterface
{
public const MODE_INSERT_OR_UPDATE = 1;
public const MODE_INSERT_ONLY = 2;
public const MODE_UPDATE_ONLY = 3;
/**
* @param array $data
* @param string $remoteId
* @param string $language
* @param string $contentType
* @param int|string $parentLocations Int for location id or string for remote location id
* @param int $mode ContentStructureFactoryInterface
*
* @return false|\CodeRhapsodie\EzDataflowBundle\Model\ContentStructure
*/
public function transform(array $data, string $remoteId, string $language, string $contentType, $parentLocations, int $mode = ContentStructureFactoryInterface::MODE_INSERT_OR_UPDATE);
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Filter;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use eZ\Publish\API\Repository\ContentService;
use Psr\Log\LoggerAwareTrait;
/**
* Filters ContentUpdateStructure that would not result in any actual changes in the content.
*/
class NotModifiedContentFilter
{
use LoggerAwareTrait;
/** @var ContentService */
private $contentService;
/** @var FieldComparatorInterface */
private $comparator;
public function __construct(ContentService $contentService, FieldComparatorInterface $comparator)
{
$this->contentService = $contentService;
$this->comparator = $comparator;
}
public function __invoke($data)
{
if (!$data instanceof ContentUpdateStructure) {
return $data;
}
if ($data->getId()) {
$content = $this->contentService->loadContent($data->getId(), [$data->getLanguageCode()]);
} else {
$content = $this->contentService->loadContentByRemoteId($data->getRemoteId(), [$data->getLanguageCode()]);
}
foreach ($data->getFields() as $identifier => $hash) {
$field = $content->getField($identifier, $data->getLanguageCode());
if ($field === null || !$this->comparator->compare($field, $hash)) {
// At least one field is different, continue the dataflow.
return $data;
}
}
// All fields are identical, filter this item out.
$this->log('info', 'Not modified content skipped', ['id' => $data->getId(), 'remote_id' => $data->getRemoteId()]);
return false;
}
private function log(string $level, string $message, array $context = [])
{
if ($this->logger === null) {
return;
}
$this->logger->log($level, $message, $context);
}
}

View File

@@ -6,68 +6,49 @@ namespace CodeRhapsodie\EzDataflowBundle\Gateway;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use CodeRhapsodie\DataflowBundle\Repository\JobRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\DBAL\Query\QueryBuilder;
class JobGateway
final class JobGateway
{
/** @var EntityManagerInterface */
private $em;
/** @var JobRepository */
private $jobRepository;
public function __construct(EntityManagerInterface $em, JobRepository $jobRepository)
public function __construct(JobRepository $jobRepository)
{
$this->em = $em;
$this->jobRepository = $jobRepository;
}
public function find(int $id): Job
public function find(int $id): ?Job
{
return $this->jobRepository->find($id);
}
public function findForScheduled(int $id): iterable
public function getOneshotListQueryForAdmin(): QueryBuilder
{
return $this->jobRepository->findBy(['scheduledDataflow' => $id], ['requestedDate' => 'desc'], 20);
return $this->jobRepository->createQueryBuilder('i')
->andWhere('i.scheduled_dataflow_id IS NULL')
->addOrderBy('i.requested_date', 'DESC');
}
public function getOneshotListQueryForAdmin(): Query
public function getListQueryForAdmin(): QueryBuilder
{
$query = $this->jobRepository->createQueryBuilder('i')
->andWhere('i.scheduledDataflow IS NULL')
->addOrderBy('i.requestedDate', 'DESC');
return $query->getQuery();
return $this->jobRepository->createQueryBuilder('w')
->addOrderBy('w.requested_date', 'DESC');
}
public function getListQueryForAdmin(): Query
public function getListQueryForScheduleAdmin(int $id): QueryBuilder
{
$query = $this->jobRepository->createQueryBuilder('w')
->addOrderBy('w.requestedDate', 'DESC');
return $query->getQuery();
}
public function getListQueryForScheduleAdmin(int $id): Query
{
$query = $this->jobRepository->createQueryBuilder('w')
->where('w.scheduledDataflow = :schedule_id')
return $this->jobRepository->createQueryBuilder('w')
->where('w.scheduled_dataflow_id = :schedule_id')
->setParameter('schedule_id', $id)
->addOrderBy('w.requestedDate', 'DESC');
return $query->getQuery();
->addOrderBy('w.requested_date', 'DESC');
}
/**
* @param Job $job
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function save(Job $job)
{
$this->em->persist($job);
$this->em->flush();
$this->jobRepository->save($job);
}
}

View File

@@ -6,54 +6,44 @@ namespace CodeRhapsodie\EzDataflowBundle\Gateway;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use CodeRhapsodie\DataflowBundle\Repository\ScheduledDataflowRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\Query\QueryBuilder;
class ScheduledDataflowGateway
final class ScheduledDataflowGateway
{
/** @var EntityManagerInterface */
private $em;
/** @var ScheduledDataflowRepository */
private $scheduledDataflowRepository;
public function __construct(EntityManagerInterface $em, ScheduledDataflowRepository $scheduledDataflowRepository)
public function __construct(ScheduledDataflowRepository $scheduledDataflowRepository)
{
$this->em = $em;
$this->scheduledDataflowRepository = $scheduledDataflowRepository;
}
public function find(int $id): ScheduledDataflow
public function find(int $id): ?ScheduledDataflow
{
return $this->scheduledDataflowRepository->find($id);
}
public function findAllOrderedByLabel(): iterable
public function getListQueryForAdmin(): QueryBuilder
{
return $this->scheduledDataflowRepository->findBy([], ['label' => 'asc']);
return $this->scheduledDataflowRepository->createQueryBuilder('s')
->addOrderBy('s.label', 'ASC');
}
/**
* @param ScheduledDataflow $scheduledDataflow
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function save(ScheduledDataflow $scheduledDataflow)
{
$this->em->persist($scheduledDataflow);
$this->em->flush();
$this->scheduledDataflowRepository->save($scheduledDataflow);
}
/**
* @param int $id
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Throwable
*/
public function delete(int $id): void
{
$workflow = $this->find($id);
$this->em->remove($workflow);
$this->em->flush();
$this->scheduledDataflowRepository->delete($id);
}
}

View File

@@ -1,7 +1,12 @@
imports:
- { resource: services/comparators.yaml }
services:
_defaults:
public: false
coderhapsodie.dataflow.connection: "@ezpublish.persistence.connection"
CodeRhapsodie\EzDataflowBundle\Controller\DashboardController:
public: true
arguments:
@@ -77,6 +82,8 @@ services:
CodeRhapsodie\EzDataflowBundle\EventSubscriber\MenuSubscriber:
tags: ['kernel.event_subscriber']
CodeRhapsodie\EzDataflowBundle\Factory\ContentStructureFactoryInterface: '@CodeRhapsodie\EzDataflowBundle\Factory\ContentStructureFactory'
CodeRhapsodie\EzDataflowBundle\Factory\ContentStructureFactory:
arguments:
$contentService: '@eZ\Publish\API\Repository\ContentService'
@@ -97,12 +104,10 @@ services:
CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway:
arguments:
$em: '@doctrine.orm.default_entity_manager'
$scheduledDataflowRepository: '@CodeRhapsodie\DataflowBundle\Repository\ScheduledDataflowRepository'
CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway:
arguments:
$em: '@doctrine.orm.default_entity_manager'
$jobRepository: '@CodeRhapsodie\DataflowBundle\Repository\JobRepository'
CodeRhapsodie\EzDataflowBundle\Tab\RepeatingTab:
@@ -122,3 +127,16 @@ services:
public: false
tags:
- {name: ezplatform.tab, group: coderhapsodie-ezdataflow}
CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter:
arguments:
$contentService: '@eZ\Publish\API\Repository\ContentService'
$comparator: '@CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface'
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface: '@CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator'
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator:
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator:
arguments:
$fieldTypeService: '@eZ\Publish\API\Repository\FieldTypeService'
abstract: true

View File

@@ -0,0 +1,42 @@
services:
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\SimpleFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezauthor' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezboolean' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezcountry' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezdate' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezdatetime' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezemail' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezfloat' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezinteger' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezisbn' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezkeyword' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezobjectrelation' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezobjectrelationlist' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezrichtext' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezselection' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztext' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezstring' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztime' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztags' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\UrlFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezurl' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\NovaSEOMetasFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'novaseometas' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\MatrixFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezmatrix' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\MapLocationFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezgmaplocation' }

View File

@@ -36,16 +36,18 @@ coderhapsodie.ezdataflow.history.details.request: 'Requested on'
coderhapsodie.ezdataflow.history.details.status: Status
coderhapsodie.ezdataflow.history.details.start: 'Started on'
coderhapsodie.ezdataflow.history.details.end: 'Finished on'
coderhapsodie.ezdataflow.history.details.count: 'Items count'
coderhapsodie.ezdataflow.history.details.count: 'Items successfully processed'
coderhapsodie.ezdataflow.history.details.options: 'Run options'
coderhapsodie.ezdataflow.history.details.errors: Errors
coderhapsodie.ezdataflow.history.details.type: 'Name of the dataflow executed'
coderhapsodie.ezdataflow.history.details.log: 'View log'
coderhapsodie.ezdataflow.workflow.repeating.new.title: 'Add a new repeating dataflow'
coderhapsodie.ezdataflow.workflow.new.cancel: Cancel
coderhapsodie.ezdataflow.workflow.new.submit: Create
coderhapsodie.ezdataflow.history.list.empty: 'No execution yet.'
coderhapsodie.ezdataflow.workflow.list.empty: 'No repeating workflow configured yet'
coderhapsodie.ezdataflow.workflow.history.title: 'Execution history'
coderhapsodie.ezdataflow.workflow.log.title: 'Execution log'
coderhapsodie.ezdataflow.workflow.list.delete: Delete
coderhapsodie.ezdataflow.workflow.delete: 'Are you sure you want to delete this dataflow schedule?'
coderhapsodie.ezdataflow.workflow.create.success: 'Dataflow schedule successfully added.'
@@ -74,3 +76,4 @@ coderhapsodie.ezdataflow.workflow.edit.submit: Save
coderhapsodie.dataflow.update.next: 'Next execution'
coderhapsodie.ezdataflow.workflow.edit.success: 'Dataflow schedule successfully updated.'
coderhapsodie.ezdataflow.workflow.edit.error: 'An error occurred during the dataflow schedule update: "%message%".'
coderhapsodie.ezdataflow.notfound: 'Requested data is not found'

View File

@@ -40,24 +40,26 @@ coderhapsodie.ezdataflow.history.details.count: 'Nombre d''objets mis à jour'
coderhapsodie.ezdataflow.history.details.options: 'Options de lancement'
coderhapsodie.ezdataflow.history.details.errors: Erreurs
coderhapsodie.ezdataflow.history.details.type: 'Nom du dataflow exécuté'
coderhapsodie.ezdataflow.history.details.log: 'Voir le log'
coderhapsodie.ezdataflow.workflow.repeating.new.title: 'Nouvel programmation d''un dataflow récurrent'
coderhapsodie.ezdataflow.workflow.new.cancel: Annuler
coderhapsodie.ezdataflow.workflow.new.submit: Créer
coderhapsodie.ezdataflow.history.list.empty: 'Aucune exécution pour le moment.'
coderhapsodie.ezdataflow.workflow.list.empty: 'Aucun dataflow n''a été programmé.'
coderhapsodie.ezdataflow.workflow.history.title: 'Historique des exécutions'
coderhapsodie.ezdataflow.workflow.log.title: 'Log de l''exécution'
coderhapsodie.ezdataflow.workflow.list.delete: Supprimer
coderhapsodie.ezdataflow.workflow.delete: 'Êtes-vous sûr de vouloir supprimer ce dataflow ?'
coderhapsodie.ezdataflow.workflow.create.success: 'La programmation du dataflow a bien été ajoutée.'
coderhapsodie.ezdataflow.workflow.create.error: 'Une erreur est survenue lors de l''ajout de la programamtion du dataflow : "%message%".'
coderhapsodie.ezdataflow.workflow.delete.success: 'La programmation du dataflow a bien été supprimé.'
coderhapsodie.ezdataflow.workflow.create.error: 'Une erreur est survenue lors de l''ajout de la programmation du dataflow : "%message%".'
coderhapsodie.ezdataflow.workflow.delete.success: 'La programmation du dataflow a bien été supprimée.'
coderhapsodie.ezdataflow.workflow.delete.error: 'Une erreur est survenue lors de la suppression de la programmation du dataflow : "%message%".'
coderhapsodie.ezdataflow.workflow.oneshot.new.title: 'Nouvelle exécution ponctuelle'
coderhapsodie.ezdataflow.job.create.success: 'Votre exécution a bien été ajoutée.'
coderhapsodie.ezdataflow.job.create.error: 'Une erreur est survenue lors de l''ajout de l''exécution : "%message%".'
coderhapsodie.dataflow.label: 'Nom de la programamtion du dataflow'
coderhapsodie.dataflow.label: 'Nom de la programmation du dataflow'
coderhapsodie.dataflow.oneshot.label: 'Nom de l''exécution ponctuelle du dataflow'
coderhapsodie.dataflow.dataflowType: 'Dataflow a exécuter'
coderhapsodie.dataflow.dataflowType: 'Dataflow à exécuter'
coderhapsodie.dataflow.options: 'Options passées au dataflow (YAML)'
coderhapsodie.dataflow.options.title: 'Entrez les options comme un tableau clé/valeur YAML'
coderhapsodie.dataflow.options.placeholder: "option1: valeur1\noption2: valeur2\n"
@@ -70,5 +72,6 @@ coderhapsodie.dataflow.create.enabled: 'Activé ?'
coderhapsodie.ezdataflow.workflow.repeating.edit.title: "Édition de la programmation d'un dataflow"
coderhapsodie.ezdataflow.workflow.edit.submit: Sauvegarder
coderhapsodie.dataflow.update.next: 'Prochaine exécution'
coderhapsodie.ezdataflow.workflow.edit.success: 'La programmation du dataflow a été mis à jour avec succès.'
coderhapsodie.ezdataflow.workflow.edit.success: 'La programmation du dataflow a été mise à jour avec succès.'
coderhapsodie.ezdataflow.workflow.edit.error: 'Une erreur est survenue lors de la modification de la programmation du dataflow : "%message%".'
coderhapsodie.ezdataflow.notfound: 'Les données demandées sont introuvables'

View File

@@ -24,7 +24,8 @@
<h3 class="modal-title">{{ 'coderhapsodie.ezdataflow.workflow.history.title'|trans }}</h3>
<button type="button" class="close" aria-label="Close">
<svg class="ez-icon ez-icon--medium" aria-hidden="true">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#discard"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#discard"></use>
</svg>
</button>
</div>
@@ -34,35 +35,44 @@
</div>
<script>
$('#ez-modal--history-details').modal({keyboard: false, show: false});
$('.history-details-aware').delegate('.modal-history-details', 'click', function(e) {
e.preventDefault();
$('#modal_content-details').html('');
$('#ez-modal--history-details').modal('show');
$.ajax(this.href, {
success: function(result) {
$('#modal_content-details').html(result);
}
(function ($) {
$(document).ready(function ($) {
$('#ez-modal--history-details').modal({keyboard: false, show: false});
$('.history-details-aware').delegate('.modal-history-details', 'click', function (e) {
e.preventDefault();
$('#modal_content-details').html('');
$('#ez-modal--history-details h3').html("{{ 'coderhapsodie.ezdataflow.workflow.history.title'|trans }}");
if ($(this).hasClass('modal-history-log')) {
$('#ez-modal--history-details h3').html("{{ 'coderhapsodie.ezdataflow.workflow.log.title'|trans }}");
}
$('#ez-modal--history-details').modal('show');
$.ajax(this.href, {
success: function (result) {
$('#modal_content-details').html(result);
}
});
});
$('#ez-modal--history-details .close').click(function () {
$('#ez-modal--history-details').modal('hide');
});
$(document).ready(function () {
if (window.location.hash && window.location.hash === '#oneshot') {
$('#ez-tab-list-coderhapsodie-ezdataflow li a').removeClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane').removeClass('active');
$('#ez-tab-list-coderhapsodie-ezdataflow li a:eq(1)').addClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane:eq(1)').addClass('active');
}
if (window.location.hash && window.location.hash === '#history') {
$('#ez-tab-list-coderhapsodie-ezdataflow li a').removeClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane').removeClass('active');
$('#ez-tab-list-coderhapsodie-ezdataflow li a:eq(2)').addClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane:eq(2)').addClass('active');
}
});
});
});
$('#ez-modal--history-details .close').click(function () {
$('#ez-modal--history-details').modal('hide');
});
$(document).ready(function () {
if (window.location.hash && window.location.hash === '#oneshot') {
$('#ez-tab-list-coderhapsodie-ezdataflow li a').removeClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane').removeClass('active');
$('#ez-tab-list-coderhapsodie-ezdataflow li a:eq(1)').addClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane:eq(1)').addClass('active');
}
if (window.location.hash && window.location.hash === '#history') {
$('#ez-tab-list-coderhapsodie-ezdataflow li a').removeClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane').removeClass('active');
$('#ez-tab-list-coderhapsodie-ezdataflow li a:eq(2)').addClass('active');
$('#ez-tab-list-content-coderhapsodie-ezdataflow .tab-pane:eq(2)').addClass('active');
}
});
})(jQuery);
</script>
{% endblock %}

View File

@@ -1,13 +1,13 @@
{% form_theme form 'bootstrap_3_layout.html.twig' %}
<h2>{{ 'coderhapsodie.ezdataflow.oneshot.title'|trans }}</h2>
<div class="ez-table-header">
<div class="ez-table-header__headline">{{ 'coderhapsodie.ezdataflow.oneshot.list.title'|trans }}</div>
<div>
<button type="button" class="btn btn-primary btn-modal-launcher" data-toggle="modal" data-target="#ez-modal--new-oneshot">
<button type="button" class="btn btn-primary btn-modal-launcher" data-toggle="modal"
data-target="#ez-modal--new-oneshot">
<svg class="ez-icon ez-icon--medium ez-icon--light ez-icon-create">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#create"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#create"></use>
</svg>
</button>
</div>
@@ -16,6 +16,7 @@
{{ include('@ezdesign/ezdataflow/parts/tab/job_list.html.twig', {identifier: 'ezdataflow_oneshot_history', paginate_route: 'coderhapsodie.ezdataflow.oneshot'}) }}
{% if form is defined %}
{% form_theme form 'bootstrap_3_layout.html.twig' %}
<div class="modal fade ez-modal show" id="ez-modal--new-oneshot" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document">
{{ include('@ezdesign/ezdataflow/parts/schedule_form.html.twig', {mode: 'oneshot'}) }}
@@ -23,27 +24,35 @@
</div>
<script>
$('#ez-modal--new-oneshot').on('submit', 'form', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
}
let obj = $(result.form).find('.modal-body');
$('#ez-modal--new-oneshot .modal-body').html($(obj).html());
}
(function ($) {
$(document).ready(function ($) {
$('#ez-modal--new-oneshot').on('submit', 'form', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
if (window.location.href === result.redirect) {
document.location.reload();
}
window.location = result.redirect;
return;
}
let obj = $(result.form).find('.modal-body');
$('#ez-modal--new-oneshot .modal-body').html($(obj).html());
}
});
});
});
});
})(jQuery);
</script>

View File

@@ -1,5 +1,3 @@
{% form_theme form 'bootstrap_3_layout.html.twig' %}
<h2>{{ 'coderhapsodie.ezdataflow.repeating.title'|trans }}</h2>
<div class="ez-table-header">
@@ -15,162 +13,172 @@
</div>
</div>
{{ include('@ezdesign/ezdataflow/parts/tab/schedule_list.html.twig') }}
{{ include('@ezdesign/ezdataflow/parts/tab/schedule_list.html.twig', {identifier: 'ezdataflow_schedule_results', paginate_route: 'coderhapsodie.ezdataflow.repeating'}) }}
<div class="modal fade ez-modal show" id="ez-modal--new-scheduled" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document">
{{ include('@ezdesign/ezdataflow/parts/schedule_form.html.twig') }}
{% if form is defined %}
{% form_theme form 'bootstrap_3_layout.html.twig' %}
<div class="modal fade ez-modal show" id="ez-modal--new-scheduled" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document">
{{ include('@ezdesign/ezdataflow/parts/schedule_form.html.twig') }}
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade ez-modal ez-modal--delete-workflow show" id="ez-modal--delete-workflow" tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<svg class="ez-icon ez-icon--medium" aria-hidden="true">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="../../ez-icons.svg#discard"></use>
</svg>
</button>
</div>
<div class="modal-body">
<p class="font-weight-bold" id="delete-modal--workflow-name"></p>
<p class="ez-modal-body__main">{{ 'coderhapsodie.ezdataflow.workflow.delete'|trans }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark"
data-dismiss="modal">{{ 'coderhapsodie.ezdataflow.workflow.new.cancel'|trans }}</button>
<button type="button" class="btn btn-danger font-weight-bold"
id="ez-modal--delete-workflow-confirm">{{ 'coderhapsodie.ezdataflow.workflow.list.delete'|trans }}</button>
<!-- Modal -->
<div class="modal fade ez-modal ez-modal--delete-workflow show" id="ez-modal--delete-workflow" tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<svg class="ez-icon ez-icon--medium" aria-hidden="true">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="../../ez-icons.svg#discard"></use>
</svg>
</button>
</div>
<div class="modal-body">
<p class="font-weight-bold" id="delete-modal--workflow-name"></p>
<p class="ez-modal-body__main">{{ 'coderhapsodie.ezdataflow.workflow.delete'|trans }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark"
data-dismiss="modal">{{ 'coderhapsodie.ezdataflow.workflow.new.cancel'|trans }}</button>
<button type="button" class="btn btn-danger font-weight-bold"
id="ez-modal--delete-workflow-confirm">{{ 'coderhapsodie.ezdataflow.workflow.list.delete'|trans }}</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade ez-modal show" id="ez-modal--history" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document" style="max-width: 80%">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ 'coderhapsodie.ezdataflow.workflow.history.title'|trans }}</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<svg class="ez-icon ez-icon--medium" aria-hidden="true">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#discard"></use>
</svg>
</button>
<div class="modal fade ez-modal show" id="ez-modal--history" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document" style="max-width: 80%">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ 'coderhapsodie.ezdataflow.workflow.history.title'|trans }}</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<svg class="ez-icon ez-icon--medium" aria-hidden="true">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#discard"></use>
</svg>
</button>
</div>
<div class="modal-body history-details-aware" id="modal_content"></div>
</div>
<div class="modal-body history-details-aware" id="modal_content"></div>
</div>
</div>
</div>
<div class="modal fade ez-modal show" id="ez-modal--edit-scheduled" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document">
<div id="schedule_edit"></div>
<div class="modal fade ez-modal show" id="ez-modal--edit-scheduled" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document">
<div id="schedule_edit"></div>
</div>
</div>
</div>
<script>
$('#ez-modal--history').modal({keyboard: false, show: false});
$('.modal-history').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#modal_content').html('');
$('#ez-modal--history').modal('show');
$.ajax(elem.href, {
success: function (result) {
$('#modal_content').html(result);
}
});
});
});
$('#ez-modal--history .close').click(function () {
$('#ez-modal--history').modal('hide');
});
<script>
(function ($) {
$(document).ready(function ($) {
$('#ez-modal--history').modal({keyboard: false, show: false});
$('.modal-history').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#modal_content').html('');
$('#ez-modal--history').modal('show');
$.ajax(elem.href, {
success: function (result) {
$('#modal_content').html(result);
}
});
});
});
$('#ez-modal--history .close').click(function () {
$('#ez-modal--history').modal('hide');
});
$('#ez-modal--delete-workflow').modal({keyboard: true, show: false});
$('.modal-delete').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#delete-modal--workflow-name').html($(elem).data('name'));
$('#ez-modal--delete-workflow').modal('show');
$('#ez-modal--delete-workflow-confirm').data('target', $(elem).data('path'));
});
});
$('#ez-modal--delete-workflow-confirm').click(function () {
let target = $(this).data('target');
if (target && target !== '') {
$.ajax(target, {
method: 'POST',
complete: function () {
$('#ez-modal--delete-workflow').modal('hide');
window.location.reload();
}
});
}
});
$('#ez-modal--edit-scheduled').modal({keyboard: true, show: false});
$('.modal-edit').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#schedule_edit').html('');
$('#ez-modal--edit-scheduled').modal('show');
$.ajax(elem.href, {
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
$('#ez-modal--delete-workflow').modal({keyboard: true, show: false});
$('.modal-delete').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#delete-modal--workflow-name').html($(elem).data('name'));
$('#ez-modal--delete-workflow').modal('show');
$('#ez-modal--delete-workflow-confirm').data('target', $(elem).data('path'));
});
});
$('#ez-modal--delete-workflow-confirm').click(function () {
let target = $(this).data('target');
if (target && target !== '') {
$.ajax(target, {
method: 'POST',
complete: function () {
$('#ez-modal--delete-workflow').modal('hide');
window.location.reload();
}
});
}
});
$('#schedule_edit').html(result.form);
}
$('#ez-modal--edit-scheduled').modal({keyboard: true, show: false});
$('.modal-edit').each(function (index, elem) {
$(elem).click(function (e) {
e.preventDefault();
$('#schedule_edit').html('');
$('#ez-modal--edit-scheduled').modal('show');
$.ajax(elem.href, {
success: function (result) {
if (result.redirect) {
if (window.location.href === result.redirect) {
document.location.reload();
}
window.location = result.redirect;
return;
}
$('#schedule_edit').html(result.form);
}
});
});
});
$('#ez-modal--edit-scheduled').on('submit', 'form', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
}
$('#schedule_edit').html(result.form);
}
});
});
$('#ez-modal--new-scheduled form').on('submit', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
}
let obj = $(result.form).find('.modal-body');
$('#ez-modal--new-scheduled .modal-body').html($(obj).html());
}
});
});
});
});
});
$('#ez-modal--edit-scheduled').on('submit', 'form', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
}
$('#schedule_edit').html(result.form);
}
});
});
$('#ez-modal--new-scheduled form').on('submit', function (e) {
e.preventDefault();
url = $(this).attr('action');
data = new FormData(this);
$.ajax({
'type': 'POST',
'url': url,
'data': data,
processData: false,
contentType: false,
success: function (result) {
if (result.redirect) {
window.location = result.redirect;
return;
}
let obj = $(result.form).find('.modal-body');
$('#ez-modal--new-scheduled .modal-body').html($(obj).html());
}
});
});
</script>
})(jQuery);
</script>
{% endif %}

View File

@@ -1,7 +1,8 @@
{% import '@ezdesign/ezdataflow/macros.twig' as macros %}
{% block content %}
<div class="container ez-main-container">
<div class="container ez-main-container history-details-aware">
{% if item is not null %}
<h2>{{ 'coderhapsodie.ezdataflow.history.job.title'|trans }}{{ item.id }}</h2>
<table class="table ez-table ez-table--list">
@@ -51,5 +52,8 @@
</tr>
</tbody>
</table>
{% else %}
<p class="ez-table-no-content">{{ 'coderhapsodie.ezdataflow.notfound'|trans }}</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
{% for line in log %}
<p>{{ line|nl2br }}</p>
{% endfor %}

View File

@@ -4,7 +4,8 @@
<div id="loading_{{ id }}" class="text-center d-none">
<svg class="ez-icon ez-icon--extra-large">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#spinner"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#spinner"></use>
</svg>
</div>
@@ -26,15 +27,26 @@
{% for job in pager.currentPageResults %}
<tr>
<td>{{ job.label }}</td>
<td>{{ job.requestedDate|date('d/m/Y H:i:s') }}</td>
<td>{{ job.requested_date|date('d/m/Y H:i:s') }}</td>
<td>{{ job.count|default('-') }}</td>
<td>{{ job.startTime ? job.startTime|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ job.endTime ? job.endTime|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ job.start_time ? job.start_time|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ job.end_time ? job.end_time|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ macros.translateStatus(job.status) }}</td>
<td class="ez-table__cell ez-table__cell--has-action-btns text-right">
<a href="{{ path('coderhapsodie.ezdataflow.job.details', {id: job.id}) }}" class="btn btn-icon mx-2 modal-history-details" title="{{ 'coderhapsodie.ezdataflow.history.list.view'|trans }}">
<a href="{{ path('coderhapsodie.ezdataflow.job.details', {id: job.id}) }}"
class="btn btn-icon mx-2 modal-history-details"
title="{{ 'coderhapsodie.ezdataflow.history.list.view'|trans }}">
<svg class="ez-icon ez-icon--small-medium">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#about-info"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#about-info"></use>
</svg>
</a>
<a href="{{ path('coderhapsodie.ezdataflow.job.log', {id: job.id}) }}"
class="btn btn-icon mx-2 modal-history-details modal-history-log"
title="{{ 'coderhapsodie.ezdataflow.history.details.log'|trans }}">
<svg class="ez-icon ez-icon--small-medium">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#article"></use>
</svg>
</a>
</td>
@@ -54,14 +66,19 @@
</div>
<script>
// Manage ajax pagination
$('#{{ id }}').delegate('.ez-pagination a', 'click', function (e) {
e.preventDefault();
$('#loading_{{ id }}').removeClass('d-none');
$('#{{ id }}')
.html('')
.load(this.href + ' #{{ id }}>*', null, function () {
$('#loading_{{ id }}').addClass('d-none');
(function ($) {
$(document).ready(function ($) {
// Manage ajax pagination
$('#{{ id }}').delegate('.ez-pagination a', 'click', function (e) {
e.preventDefault();
$('#loading_{{ id }}').removeClass('d-none');
$('#{{ id }}')
.html('')
.load(this.href + ' #{{ id }}>*', null, function () {
$('#loading_{{ id }}').addClass('d-none');
});
});
});
});
})(jQuery);
</script>

View File

@@ -1,55 +1,106 @@
{% if items|length %}
<table class="table">
<thead>
<tr>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.name'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.frequency'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.next_execution'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.enabled'|trans }}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in items %}
{% set id = identifier|default('ezdataflow_schedule_results') %}
<div id="loading_{{ id }}" class="text-center d-none">
<svg class="ez-icon ez-icon--extra-large">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#spinner"></use>
</svg>
</div>
<div id="{{ id }}" class="history-details-aware">
{% if pager.currentPageResults|length %}
<table class="table">
<thead>
<tr>
<td>{{ item.label }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.next|date('d/m/Y H:i:s') }}</td>
<td>{{ ('coderhapsodie.ezdataflow.' ~ (item.enabled ? 'yes' : 'no'))|trans }}</td>
<td class="ez-table__cell ez-table__cell--has-action-btns text-right">
<a href="{{ path('coderhapsodie.ezdataflow.history.workflow', {id: item.id}) }}" class="btn btn-icon mx-2 modal-history" title="{{ 'coderhapsodie.ezdataflow.workflow.list.history'|trans }}">
<svg class="ez-icon ez-icon--small-medium">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#history"></use>
</svg>
</a>
<a href="{{ path('coderhapsodie.ezdataflow.workflow.edit', {id: item.id}) }}" type="button" class="btn btn-icon mx-2 modal-edit" title="{{ 'coderhapsodie.ezdataflow.workflow.list.edit'|trans }}">
<svg class="ez-icon ez-icon--small-medium ez-icon-edit">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#edit"></use>
</svg>
</a>
{% if item.enabled %}
<a href="{{ path('coderhapsodie.ezdataflow.workflow.disable', {id: item.id}) }}" class="btn btn-icon mx-2" title="{{ 'coderhapsodie.ezdataflow.workflow.list.disable'|trans }}">
<svg class="ez-icon ez-icon--small-medium" style="fill: #f7d000;">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#logout"></use>
</svg>
</a>
{% else %}
<a href="{{ path('coderhapsodie.ezdataflow.workflow.enable', {id: item.id}) }}" class="btn btn-icon mx-2" title="{{ 'coderhapsodie.ezdataflow.workflow.list.enable'|trans }}">
<svg class="ez-icon ez-icon--small-medium" style="fill: #00825c;">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#approved"></use>
</svg>
</a>
{% endif %}
<button type="button" class="btn btn-icon mx-2 ez-btn--content-trash modal-delete" data-name="{{ item.label }}" data-path="{{ path('coderhapsodie.ezdataflow.workflow.delete', {id: item.id}) }}" title="{{ 'coderhapsodie.ezdataflow.workflow.list.delete'|trans }}">
<svg class="ez-icon ez-icon--small-medium ez-icon-trash">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#trash"></use>
</svg>
</button>
</td>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.name'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.frequency'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.next_execution'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.workflow.list.enabled'|trans }}</th>
<th></th>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="ez-table-no-content">{{ 'coderhapsodie.ezdataflow.workflow.list.empty'|trans }}</p>
{% endif %}
</thead>
<tbody>
{% for item in pager.currentPageResults %}
<tr>
<td>{{ item.label }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.next|date('d/m/Y H:i:s') }}</td>
<td>{{ ('coderhapsodie.ezdataflow.' ~ (item.enabled ? 'yes' : 'no'))|trans }}</td>
<td class="ez-table__cell ez-table__cell--has-action-btns text-right">
<a href="{{ path('coderhapsodie.ezdataflow.history.workflow', {id: item.id}) }}"
class="btn btn-icon mx-2 modal-history"
title="{{ 'coderhapsodie.ezdataflow.workflow.list.history'|trans }}">
<svg class="ez-icon ez-icon--small-medium">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#history"></use>
</svg>
</a>
<a href="{{ path('coderhapsodie.ezdataflow.workflow.edit', {id: item.id}) }}" type="button"
class="btn btn-icon mx-2 modal-edit"
title="{{ 'coderhapsodie.ezdataflow.workflow.list.edit'|trans }}">
<svg class="ez-icon ez-icon--small-medium ez-icon-edit">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#edit"></use>
</svg>
</a>
{% if item.enabled %}
<a href="{{ path('coderhapsodie.ezdataflow.workflow.disable', {id: item.id}) }}"
class="btn btn-icon mx-2"
title="{{ 'coderhapsodie.ezdataflow.workflow.list.disable'|trans }}">
<svg class="ez-icon ez-icon--small-medium" style="fill: #f7d000;">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#logout"></use>
</svg>
</a>
{% else %}
<a href="{{ path('coderhapsodie.ezdataflow.workflow.enable', {id: item.id}) }}"
class="btn btn-icon mx-2"
title="{{ 'coderhapsodie.ezdataflow.workflow.list.enable'|trans }}">
<svg class="ez-icon ez-icon--small-medium" style="fill: #00825c;">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#approved"></use>
</svg>
</a>
{% endif %}
<button type="button" class="btn btn-icon mx-2 ez-btn--content-trash modal-delete"
data-name="{{ item.label }}"
data-path="{{ path('coderhapsodie.ezdataflow.workflow.delete', {id: item.id}) }}"
title="{{ 'coderhapsodie.ezdataflow.workflow.list.delete'|trans }}">
<svg class="ez-icon ez-icon--small-medium ez-icon-trash">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#trash"></use>
</svg>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="ez-table-no-content">{{ 'coderhapsodie.ezdataflow.workflow.list.empty'|trans }}</p>
{% endif %}
{% if pager.haveToPaginate %}
<div class="ez-pagination justify-content-center align-items-center ez-pagination__spacing">
{{ pagerfanta(pager, 'ez', {routeName: paginate_route, routeParams: paginate_params|default({})}) }}
</div>
{% endif %}
</div>
<script>
(function ($) {
$(document).ready(function ($) {
// Manage ajax pagination
$('#{{ id }}').delegate('.ez-pagination a', 'click', function (e) {
e.preventDefault();
$('#loading_{{ id }}').removeClass('d-none');
$('#{{ id }}')
.html('')
.load(this.href + ' #{{ id }}>*', null, function () {
$('#loading_{{ id }}').addClass('d-none');
});
});
});
})(jQuery);
</script>

View File

@@ -4,15 +4,19 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Writer;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\DelegateWriterInterface;
use CodeRhapsodie\EzDataflowBundle\Core\Content\ContentCreatorInterface;
use CodeRhapsodie\EzDataflowBundle\Core\Content\ContentUpdaterInterface;
use CodeRhapsodie\EzDataflowBundle\Model\ContentCreateStructure;
use CodeRhapsodie\EzDataflowBundle\Model\ContentStructure;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\WriterInterface;
class ContentWriter extends RepositoryWriter implements WriterInterface
use Psr\Log\LoggerAwareTrait;
class ContentWriter extends RepositoryWriter implements DelegateWriterInterface
{
use LoggerAwareTrait;
/** @var ContentCreatorInterface */
private $creator;
@@ -31,15 +35,37 @@ class ContentWriter extends RepositoryWriter implements WriterInterface
public function write($item)
{
if (!$item instanceof ContentStructure) {
$this->log('warning', "Data is not a ContentStucture");
return;
}
if ($item instanceof ContentCreateStructure) {
$this->creator->createFromStructure($item);
$this->log('info', 'Save content', [
'content_type' => $item->getContentTypeIdentifier(),
'content_location' => $item->getLocations()
]);
return $this->creator->createFromStructure($item);
}
if ($item instanceof ContentUpdateStructure) {
$this->updater->updateFromStructure($item);
$this->log('info', 'Update content', ['id' => $item->getId(), 'remote_id' => $item->getRemoteId()]);
return $this->updater->updateFromStructure($item);
}
}
/**
* {@inheritdoc}
*/
public function supports($item): bool
{
return $item instanceof ContentStructure;
}
private function log(string $level, string $message, array $context = [])
{
if ($this->logger === null) {
return;
}
$this->logger->log($level, $message, $context);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace CodeRhapsodie\EzDataflowBundle\Tests\Core\FieldComparator;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use eZ\Publish\API\Repository\Values\Content\Field;
use PHPUnit\Framework\TestCase;
class DelegatorFieldComparatorTest extends TestCase
{
/** @var DelegatorFieldComparator */
private $delegatorFieldComparator;
protected function setUp(): void
{
$type1FieldComparatorMock = $this->createMock(FieldComparatorInterface::class);
$type1FieldComparatorMock->method('compare')->willReturnCallback(function (Field $field, $hash) {
return $hash === 'rightValue1';
});
$type2FieldComparatorMock = $this->createMock(FieldComparatorInterface::class);
$type2FieldComparatorMock->method('compare')->willReturnCallback(function (Field $field, $hash) {
return $hash === 'rightValue2';
});
$this->delegatorFieldComparator = new DelegatorFieldComparator();
$this->delegatorFieldComparator->registerDelegateFieldComparator($type1FieldComparatorMock, 'type1');
$this->delegatorFieldComparator->registerDelegateFieldComparator($type2FieldComparatorMock, 'type2');
}
/**
* @dataProvider fieldProvider
*/
public function testField(string $type, bool $expected, $hash)
{
$field = new Field(['fieldTypeIdentifier' => $type]);
$return = $this->delegatorFieldComparator->compare($field, $hash);
$this->assertSame($expected, $return);
}
public function fieldProvider(): iterable
{
yield ['type1', true, 'rightValue1'];
yield ['type1', false, 'wrongValue'];
yield ['type2', true, 'rightValue2'];
yield ['type2', false, 'wrongValue'];
yield ['otherType', false, 'rightValue1'];
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Tests\Filter;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Values\Content\Field;
use eZ\Publish\Core\Repository\Values\Content\Content;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class NotModifiedContentFilterTest extends TestCase
{
/** @var ContentService|MockObject */
private $contentServiceMock;
/** @var FieldComparatorInterface|MockObject */
private $comparatorMock;
/** @var NotModifiedContentFilter */
private $notModifiedContentFilter;
protected function setUp(): void
{
$this->contentServiceMock = $this->createMock(ContentService::class);
$this->comparatorMock = $this->createMock(FieldComparatorInterface::class);
$this->notModifiedContentFilter = new NotModifiedContentFilter($this->contentServiceMock, $this->comparatorMock);
}
public function testNotContentUpdateStructure()
{
$data = 'notAStruct';
$returnValue = ($this->notModifiedContentFilter)($data);
$this->assertSame($data, $returnValue);
}
public function testIdenticalContent()
{
$id = 10;
$field1 = 'field1';
$value1 = 'value1';
$field2 = 'field2';
$value2 = 'value2';
$contentField1 = new Field();
$contentField2 = new Field();
$data = ContentUpdateStructure::createForContentId($id, 'lang', [
$field1 => $value1,
$field2 => $value2,
]);
$content = $this->createMock(Content::class);
$content
->expects($this->exactly(2))
->method('getField')
->withConsecutive([$field1], [$field2])
->willReturnOnConsecutiveCalls($contentField1, $contentField2)
;
$this->contentServiceMock
->expects($this->once())
->method('loadContent')
->with($id)
->willReturn($content)
;
$this->comparatorMock
->expects($this->exactly(2))
->method('compare')
->withConsecutive([$contentField1, $value1], [$contentField2, $value2])
->willReturn(true)
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertFalse($return);
}
public function testDifferentContent()
{
$id = 10;
$field1 = 'field1';
$value1 = 'value1';
$field2 = 'field2';
$value2 = 'value2';
$field3 = 'field3';
$value3 = 'value3';
$contentField1 = new Field();
$contentField2 = new Field();
$data = ContentUpdateStructure::createForContentId($id, 'lang', [
$field1 => $value1,
$field2 => $value2,
$field3 => $value3,
]);
$content = $this->createMock(Content::class);
$content
->expects($this->exactly(2))
->method('getField')
->withConsecutive([$field1], [$field2])
->willReturnOnConsecutiveCalls($contentField1, $contentField2)
;
$this->contentServiceMock
->expects($this->once())
->method('loadContent')
->with($id)
->willReturn($content)
;
$this->comparatorMock
->expects($this->exactly(2))
->method('compare')
->withConsecutive([$contentField1, $value1], [$contentField2, $value2])
->willReturnOnConsecutiveCalls(true, false)
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertSame($data, $return);
}
public function testLoadEmptyByRemoteId()
{
$remoteId = 'abc';
$data = ContentUpdateStructure::createForContentRemoteId($remoteId, 'lang', []);
$this->contentServiceMock
->expects($this->once())
->method('loadContentByRemoteId')
->with($remoteId)
->willReturn(new Content())
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertFalse($return);
}
}

11
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
// Skip autoloading if already done by phpunit alias (including from meta repo if this is vendor)
if (defined('PHPUNIT_COMPOSER_INSTALL')) {
return;
}
$autoloadFile = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($autoloadFile)) {
throw new RuntimeException('Install dependencies to run test suite.');
}
require_once $autoloadFile;