From 846e23b0caab49639ba18ddb46fc18bb38d14648 Mon Sep 17 00:00:00 2001 From: Dima Soroka Date: Tue, 23 Jul 2013 14:12:17 -0700 Subject: [PATCH 001/541] BAP-1258: Added templates hinting to output in dev mode in order to help frontend developers to find proper template files --- src/Oro/Bundle/UIBundle/README.md | 23 +++++++++ .../UIBundle/Tests/Unit/Twig/TemplateTest.php | 47 +++++++++++++++++++ src/Oro/Bundle/UIBundle/Twig/Template.php | 33 +++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php create mode 100644 src/Oro/Bundle/UIBundle/Twig/Template.php diff --git a/src/Oro/Bundle/UIBundle/README.md b/src/Oro/Bundle/UIBundle/README.md index 8e659e41291..211752f1d37 100644 --- a/src/Oro/Bundle/UIBundle/README.md +++ b/src/Oro/Bundle/UIBundle/README.md @@ -68,3 +68,26 @@ Additional options can be passed to all placeholder child items using 'with' e.g ```html {% placeholder with {'form' : form} %} ``` + +## Templates Hinting + +UIBundle allows to enable templates hinting and in such a way helps to frontend developer to find proper template. +This option can be enabled in application configuration with redefining base template class for twig: + + ```yaml + twig: + base_template_class: Oro\Bundle\UIBundle\Twig\Template + ``` + +As e result of such change user can find HTML comments on the page +```html + +... + +``` +or see "template_name" variable for AJAX requests that expecting JSON +```json +"template_name":"BundleName:template_name.html.twig" +``` + +Templates hinting is enabled by default in development mode. \ No newline at end of file diff --git a/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php new file mode 100644 index 00000000000..888211b8e3d --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php @@ -0,0 +1,47 @@ + 'test')); + } +} + + +class TemplateTest extends \PHPUnit_Framework_TestCase +{ + public function testStringRender() + { + $object = new StringTemplateTest(new \Twig_Environment()); + $output = $object->render(array()); + $this->assertContains('', $output); + } + + public function testJsonRender() + { + $object = new JsonTemplateTest(new \Twig_Environment()); + $output = $object->render(array()); + $output = json_decode($output); + $this->assertTrue($output->template_name == 'json.twig'); + } +} diff --git a/src/Oro/Bundle/UIBundle/Twig/Template.php b/src/Oro/Bundle/UIBundle/Twig/Template.php new file mode 100644 index 00000000000..4ec2f106af3 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Twig/Template.php @@ -0,0 +1,33 @@ +template_name = $this->getTemplateName(); + $content = json_encode($templateJson); + } else { + $content = '\n"; + $content.= $templateContent; + $content.= ''; + } + return $content; + } +} From 8f1ba61d26642f017e7750288d45987a0fa9d379 Mon Sep 17 00:00:00 2001 From: Dima Soroka Date: Tue, 23 Jul 2013 14:42:19 -0700 Subject: [PATCH 002/541] BAP-1258: Improved templates hinting for AJAX requests --- .../UIBundle/Tests/Unit/Twig/TemplateTest.php | 3 +- src/Oro/Bundle/UIBundle/Twig/Template.php | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php index 888211b8e3d..9b9d2041436 100644 --- a/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php +++ b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/TemplateTest.php @@ -23,7 +23,7 @@ public function getTemplateName() protected function doDisplay(array $context, array $blocks = array()) { - echo json_encode(array('json' => 'test')); + echo json_encode(array('content' => 'test')); } } @@ -43,5 +43,6 @@ public function testJsonRender() $output = $object->render(array()); $output = json_decode($output); $this->assertTrue($output->template_name == 'json.twig'); + $this->assertContains('', $output->content); } } diff --git a/src/Oro/Bundle/UIBundle/Twig/Template.php b/src/Oro/Bundle/UIBundle/Twig/Template.php index 4ec2f106af3..5a6d6048851 100644 --- a/src/Oro/Bundle/UIBundle/Twig/Template.php +++ b/src/Oro/Bundle/UIBundle/Twig/Template.php @@ -18,16 +18,31 @@ public function render(array $context) $templateJson = json_decode($templateContent); if ($templateJson) { $templateJson->template_name = $this->getTemplateName(); + if ($templateJson->content) { + $templateJson->content = $this->wrapContent($templateJson->content); + } $content = json_encode($templateJson); } else { - $content = '\n"; - $content.= $templateContent; - $content.= ''; + $content = $this->wrapContent($templateContent); + } + return $content; + } + + /** + * Wraps content into additional HTML comment tags with template name information + * + * @param string $originalContent + * @return string + */ + protected function wrapContent($originalContent) + { + $content = '\n"; + $content.= $originalContent; + $content.= ''; return $content; } } From fc847d6f35f7d93e4b5143bcc27cb857dd881b12 Mon Sep 17 00:00:00 2001 From: Dima Soroka Date: Wed, 24 Jul 2013 15:51:42 -0700 Subject: [PATCH 003/541] BAP-1258: Added ability to enable debug for all CSS and JS assets in oro assetic --- .../DependencyInjection/Configuration.php | 16 ++++---- .../OroAsseticExtension.php | 12 +++--- src/Oro/Bundle/AsseticBundle/README.md | 40 ++++++++++++++----- .../OroAsseticExtensionTest.php | 7 +++- 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php index 25c3c358c29..47fd9a24abd 100644 --- a/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php @@ -22,14 +22,16 @@ public function getConfigTreeBuilder() $rootNode ->children() - ->arrayNode('uncompress_js') - ->prototype('scalar') + ->arrayNode('js_debug') + ->prototype('scalar')->end() + ->end() + ->booleanNode('js_debug_all')->defaultValue(false)->end() + ->arrayNode('css_debug') + ->prototype('scalar')->end() + ->end() + ->booleanNode('css_debug_all')->defaultValue(false)->end() ->end() - ->end() - ->arrayNode('uncompress_css') - ->prototype('scalar') - ->end() - ->end(); + ; return $treeBuilder; } diff --git a/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php b/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php index 9134301c6e5..c17645ebda4 100644 --- a/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php +++ b/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php @@ -83,23 +83,23 @@ public function getAssets(ContainerBuilder $container, $config) $container->setParameter( 'oro_assetic.compiled_assets_groups', array( - 'js' => $config['uncompress_js'], - 'css' => $config['uncompress_css'] + 'js' => $config['js_debug'], + 'css' => $config['css_debug'] ) ); return array( - 'css' => $this->getAssetics($css, $config['uncompress_css']), - 'js' => $this->getAssetics($js, $config['uncompress_js']), + 'js' => $this->getAssetics($js, $config['js_debug'], $config['js_debug_all']), + 'css' => $this->getAssetics($css, $config['css_debug'], $config['css_debug_all']), ); } - protected function getAssetics($assetsArray, $uncompressBlocks) + protected function getAssetics($assetsArray, $debugBlocks, $debugAll) { $compressAssets = array(); $uncompressAssets = array(); foreach ($assetsArray as $blockName => $files) { - if (in_array($blockName, $uncompressBlocks)) { + if ($debugAll || in_array($blockName, $debugBlocks)) { $uncompressAssets = array_merge($uncompressAssets, $files); } else { $compressAssets = array_merge($compressAssets, $files); diff --git a/src/Oro/Bundle/AsseticBundle/README.md b/src/Oro/Bundle/AsseticBundle/README.md index 6f3b2f608db..dbc1745d6df 100644 --- a/src/Oro/Bundle/AsseticBundle/README.md +++ b/src/Oro/Bundle/AsseticBundle/README.md @@ -1,9 +1,13 @@ OroAsseticBundle ======================== -To implement hashtag navigation all basic javascript and css files should be loaded in main template. -Each bundle can have a config file assets.yml with the list of js and css files. +OroAssetic enables expandable and optimized way to manage CSS and JS assets that are distributed across many bundles. +With OroAssetic developer can define CSS and JavaScript files groups in assets.yml configuration of the bundle. Defined +files will be automatically merged and optimized for web presentation. For development and debug purposes some files can +be excluded from optimization process. + +Example of assets.yml file: ```yaml js: 'some_group' @@ -17,23 +21,37 @@ css: - 'Assets/Path/To/Css/third.css' ``` -Js and css sections contain groups of files. This groups can be uncompressed for debugging. +Js and css sections contain groups of files. This groups can be excluded from optimization process debugging purposes. + +The path to file can be defined as @BundleName/Resources/puclic/path/to/file.ext or bundles/bundle/path/to/file.ext. +If the file path contains @, then in debug mode it will be taken via controller. If path doesn't contain @, then file +will be taken via request to web folder. -The path to file can be in @BundleName/Resources/puclic/path/to/file.ext or bundles/bundle/path/to/file.ext. If the file path -contains @, then in uncompiled mode it will be taken via controller. If path doesn't contain @, then file will be taken -via request to web folder. +For example, to turn off compression of css files in 'css_group' group the following configuration should be added +to app/config/config.yml (or app/config/config_{mode}.yml) file: -For example, to turn off compression of css files from 'css_group' the next configuration mut be added in config.yml file: +```yaml +oro_assetic: + js_debug: ~ + css_debug: [css_group] +``` +In order to anable debug mode for all CSS and JS files following configuration can be applied: ```yaml oro_assetic: - uncompress_js: ~ - uncompress_css: [css_group] + js_debug_all: true + css_debug_all: true +``` + +Cache cleanup and Oro assetics dump required after: + +```php +php app/console cache:clear +php app/console oro:assetic:dump ``` -and cache must be cleaned. -To get list of all available assets groups next command should be used: +To get list of all available asset groups next command should be used: ```php php app/console oro:assetic:dump show-groups diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php index 0248706570b..0bf16f80f19 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php @@ -21,9 +21,12 @@ public function testGetAssets() ) ); - $assets = $extension->getAssets($container, array('uncompress_css' => array(), 'uncompress_js' => array())); + $assets = $extension->getAssets( + $container, + array('css_debug' => array(), 'js_debug' => array(), 'css_debug_all' => false, 'js_debug_all' => true) + ); $this->assertEquals('second.css', $assets['css']['compress'][0][1]); - $this->assertEquals('first.js', $assets['js']['compress'][0][0]); + $this->assertEquals('first.js', $assets['js']['uncompress'][0][0]); } } From 3b7e6e79143c74dc7adbd5b321fe36970c50384c Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Thu, 25 Jul 2013 19:18:30 +0300 Subject: [PATCH 004/541] BAP-1181: Define structure for interchangeable report format --- .../Datagrid/QueryConverter/YamlConverter.php | 95 +++++++++++++++++++ .../Datagrid/QueryConverterInterface.php | 27 ++++++ 2 files changed, 122 insertions(+) create mode 100644 src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php create mode 100644 src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php diff --git a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php new file mode 100644 index 00000000000..0a235646a2c --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php @@ -0,0 +1,95 @@ +createQueryBuilder(); + + if (!isset($value['from'])) { + throw new \RuntimeException('Missing mandatory "from" section'); + } + + foreach ((array) $value['from'] as $from) { + $qb->from($from['table'], $from['alias']); + } + + if (isset($value['select'])) { + $qb->select($value['select']); + } + + if (isset($value['distinct'])) { + $qb->distinct((bool) $value['distinct']); + } + + if (isset($value['join'])) { + if (isset($value['join']['inner'])) { + foreach ((array) $value['join']['inner'] as $join) { + $qb->innerJoin($join['join'], $join['alias']); + } + } + + if (isset($value['join']['left'])) { + foreach ((array) $value['join']['left'] as $join) { + $qb->leftJoin($join['join'], $join['alias']); + } + } + } + + if (isset($value['groupBy'])) { + $qb->groupBy($value['groupBy']); + } + + if (isset($value['having'])) { + $qb->having($value['having']); + } + + if (isset($value['where'])) { + if (isset($value['where']['and'])) { + foreach ((array) $value['where']['and'] as $where) { + $qb->andWhere($where); + } + } + + if (isset($value['where']['or'])) { + foreach ((array) $value['where']['or'] as $where) { + $qb->orWhere($where); + } + } + } + + if (isset($value['orderBy'])) { + $qb->resetDQLPart('orderBy'); + + foreach ((array) $value['orderBy'] as $order) { + $qb->addOrderBy($order['column'], $order['dir']); + } + } + + return $qb; + } + + /** + * {@inheritdoc} + */ + public function dump(QueryBuilder $input) + { + ; + } +} \ No newline at end of file diff --git a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php new file mode 100644 index 00000000000..c5d4b1552df --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php @@ -0,0 +1,27 @@ + Date: Fri, 26 Jul 2013 17:16:28 +0300 Subject: [PATCH 005/541] BAP-1247: Implement query parser - added tests --- .../QueryConverter/YamlConverterTest.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php new file mode 100644 index 00000000000..4f43d153e13 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php @@ -0,0 +1,39 @@ +model); + } + + public function testGetQueryBuilder() + { + $queryBuilderMock = $this->getMock('Doctrine\ORM\QueryBuilder', array(), array(), '', false); + $this->model = new ProxyQuery($queryBuilderMock); + $this->assertEquals($queryBuilderMock, $this->model->getQueryBuilder()); + } + + public function testSetParameter() + { + $testName = 'test_name'; + $testValue = 'test_value'; + + $queryBuilderMock = $this->getMock('Doctrine\ORM\QueryBuilder', array('setParameter'), array(), '', false); + $queryBuilderMock->expects($this->once()) + ->method('setParameter') + ->with($testName, $testValue); + + $this->model = new ProxyQuery($queryBuilderMock); + $this->model->setParameter($testName, $testValue); + } +} From 219b8a74fbbe88c93fddea2db51982328d66dbda Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Fri, 26 Jul 2013 16:57:29 +0200 Subject: [PATCH 006/541] BAP-134 : refactor flexible query builder to use by default the classic query builder and add joins on demand --- .../Doctrine/ORM/FlexibleQueryBuilder.php | 81 ++++++++++------- .../Repository/FlexibleEntityRepository.php | 87 +++---------------- 2 files changed, 65 insertions(+), 103 deletions(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php index 34f018e3919..2fc1e57ae41 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php @@ -7,14 +7,16 @@ use Oro\Bundle\FlexibleEntityBundle\Exception\FlexibleQueryException; /** - * Extends query builder to add useful shortcuts which allow to easily select, filter or sort a flexible entity values - * - * It works exactly as classic QueryBuilder - * - * + * Aims to customize a query builder to add useful shortcuts which allow to easily select, filter or sort a flexible + * entity values */ -class FlexibleQueryBuilder extends QueryBuilder +class FlexibleQueryBuilder { + /** + * QueryBuilder + * @var QueryBuilder + */ + protected $qb; /** * Locale code @@ -34,6 +36,27 @@ class FlexibleQueryBuilder extends QueryBuilder */ protected $aliasCounter = 1; + /** + * Constructor + * @param QueryBuilder $qb + */ + public function __construct(QueryBuilder $qb, $locale, $scope) + { + $this->qb = $qb; + $this->locale = $locale; + $this->scope = $scope; + } + + /** + * Get query builder + * + * @return string + */ + public function getQueryBuilder() + { + return $this->qb; + } + /** * Get locale code * @@ -100,13 +123,13 @@ public function prepareAttributeJoinCondition(AbstractAttribute $attribute, $joi if ($this->getLocale() === null) { throw new FlexibleQueryException('Locale must be configured'); } - $condition .= ' AND '.$joinAlias.'.locale = '.$this->expr()->literal($this->getLocale()); + $condition .= ' AND '.$joinAlias.'.locale = '.$this->qb->expr()->literal($this->getLocale()); } if ($attribute->getScopable()) { if ($this->getScope() === null) { throw new FlexibleQueryException('Scope must be configured'); } - $condition .= ' AND '.$joinAlias.'.scope = '.$this->expr()->literal($this->getScope()); + $condition .= ' AND '.$joinAlias.'.scope = '.$this->qb->expr()->literal($this->getScope()); } return $condition; @@ -191,37 +214,37 @@ public function prepareCriteriaCondition($field, $operator, $value) switch ($operator) { case '=': - $condition = $this->expr()->eq($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->eq($field, $this->qb->expr()->literal($value))->__toString(); break; case '<': - $condition = $this->expr()->lt($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->lt($field, $this->qb->expr()->literal($value))->__toString(); break; case '<=': - $condition = $this->expr()->lte($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->lte($field, $this->qb->expr()->literal($value))->__toString(); break; case '>': - $condition = $this->expr()->gt($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->gt($field, $this->qb->expr()->literal($value))->__toString(); break; case '>=': - $condition = $this->expr()->gte($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->gte($field, $this->qb->expr()->literal($value))->__toString(); break; case 'LIKE': - $condition = $this->expr()->like($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->like($field, $this->qb->expr()->literal($value))->__toString(); break; case 'NOT LIKE': - $condition = sprintf('%s NOT LIKE %s', $field, $this->expr()->literal($value)); + $condition = sprintf('%s NOT LIKE %s', $field, $this->qb->expr()->literal($value)); break; case 'NULL': - $condition = $this->expr()->isNull($field); + $condition = $this->qb->expr()->isNull($field); break; case 'NOT NULL': - $condition = $this->expr()->isNotNull($field); + $condition = $this->qb->expr()->isNotNull($field); break; case 'IN': - $condition = $this->expr()->in($field, $value)->__toString(); + $condition = $this->qb->expr()->in($field, $value)->__toString(); break; case 'NOT IN': - $condition = $this->expr()->notIn($field, $value)->__toString(); + $condition = $this->qb->expr()->notIn($field, $value)->__toString(); break; default: throw new FlexibleQueryException('operator '.$operator.' is not supported'); @@ -259,13 +282,13 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val // inner join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->innerJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->innerJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then join to option with filter on option id $joinAliasOpt = 'filterO'.$attribute->getCode().$this->aliasCounter; $backendField = sprintf('%s.%s', $joinAliasOpt, 'id'); $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); - $this->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + $this->qb->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); } else { @@ -273,7 +296,7 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val $backendField = sprintf('%s.%s', $joinAlias, $attribute->getBackendType()); $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); $condition .= ' AND '.$this->prepareCriteriaCondition($backendField, $operator, $value); - $this->innerJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->innerJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); } return $this; @@ -295,25 +318,25 @@ public function addAttributeOrderBy(AbstractAttribute $attribute, $direction) // join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->leftJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->leftJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then to option and option value to sort on $joinAliasOpt = $aliasPrefix.'O'.$attribute->getCode().$this->aliasCounter; $condition = $joinAliasOpt.".attribute = ".$attribute->getId(); - $this->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + $this->qb->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); $joinAliasOptVal = $aliasPrefix.'OV'.$attribute->getCode().$this->aliasCounter; - $condition = $joinAliasOptVal.'.locale = '.$this->expr()->literal($this->getLocale()); - $this->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); + $condition = $joinAliasOptVal.'.locale = '.$this->qb->expr()->literal($this->getLocale()); + $this->qb->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); - $this->addOrderBy($joinAliasOptVal.'.value', $direction); + $this->qb->addOrderBy($joinAliasOptVal.'.value', $direction); } else { // join to value and sort on $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->leftJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - $this->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); + $this->qb->leftJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); } } } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php index 50fd7c3029c..a29f338cde9 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php @@ -37,12 +37,6 @@ class FlexibleEntityRepository extends EntityRepository implements TranslatableI */ protected $scope; - /** - * Entity alias - * @var string - */ - protected $entityAlias; - /** * Get flexible entity config * @@ -75,7 +69,6 @@ public function setFlexibleConfig($config) public function getLocale() { if (!$this->locale) { - // use default locale $this->locale = $this->flexibleConfig['default_locale']; } @@ -104,7 +97,6 @@ public function setLocale($code) public function getScope() { if (!$this->scope) { - // use default scope $this->scope = $this->flexibleConfig['default_scope']; } @@ -184,48 +176,6 @@ public function findAttributeByCode($code) return $attribute; } - /** - * TODO : should be remove to use the basic one by default and explicitely use createFlexibleQueryBuilder to add - * join to related tables, should be updated in grid - * - * @param string $alias alias for entity - * - * @return QueryBuilder $qb - */ - public function createQueryBuilder($alias) - { - return $this->createFlexibleQueryBuilder($alias); - } - - /** - * Create a new QueryBuilder instance that allow to automatically join on attribute values and allow doctrine - * hydratation as real flexible entity, value, option and attributes - * - * @param string $alias alias for entity - * @param boolean $attributeCodes add selects on values only for this attribute codes list - * - * @return FlexibleQueryBuilder $qb - */ - public function createFlexibleQueryBuilder($alias, $attributeCodes = null) - { - $this->entityAlias = $alias; - $qb = new FlexibleQueryBuilder($this->_em); - - $qb->setLocale($this->getLocale()); - $qb->setScope($this->getScope()); - - $qb->select($alias, 'Value', 'Attribute', 'ValueOption', 'AttributeOptionValue') - ->from($this->_entityName, $this->entityAlias); - $this->addJoinToValueTables($qb, $alias); - - if (!empty($attributeCodes)) { - $qb->where($qb->expr()->in('Attribute.code', $attributeCodes)); - $qb->orWhere($qb->expr()->isNull('Attribute.code')); - } - - return $qb; - } - /** * Add join to values tables * @@ -233,7 +183,7 @@ public function createFlexibleQueryBuilder($alias, $attributeCodes = null) */ protected function addJoinToValueTables(QueryBuilder $qb) { - $qb->leftJoin($this->entityAlias.'.values', 'Value') + $qb->leftJoin(current($qb->getRootAliases()).'.values', 'Value') ->leftJoin('Value.attribute', 'Attribute') ->leftJoin('Value.options', 'ValueOption') ->leftJoin('ValueOption.optionValues', 'AttributeOptionValue'); @@ -252,31 +202,19 @@ protected function addJoinToValueTables(QueryBuilder $qb) */ public function findByWithAttributesQB(array $attributes = array(), array $criteria = null, array $orderBy = null, $limit = null, $offset = null) { - $qb = $this->createFlexibleQueryBuilder('Entity', $attributes); + $qb = $this->createQueryBuilder('Entity'); + $this->addJoinToValueTables($qb); $codeToAttribute = $this->getCodeToAttributes($attributes); $attributes = array_keys($codeToAttribute); - // add criterias if (!is_null($criteria)) { foreach ($criteria as $attCode => $attValue) { - if (in_array($attCode, $attributes)) { - $attribute = $codeToAttribute[$attCode]; - $qb->addAttributeFilter($attribute, '=', $attValue); - } else { - $qb->andWhere($qb->expr()->eq($this->entityAlias.'.'.$attCode, $qb->expr()->literal($attValue))); - } + $this->applyFilterByAttribute($qb, $attCode, $attValue); } } - - // add sorts if (!is_null($orderBy)) { foreach ($orderBy as $attCode => $direction) { - if (in_array($attCode, $attributes)) { - $attribute = $codeToAttribute[$attCode]; - $qb->addAttributeOrderBy($attribute, $direction); - } else { - $qb->addOrderBy($this->entityAlias.'.'.$attCode, $direction); - } + $this->applySorterByAttribute($qb, $attCode, $direction); } } @@ -311,7 +249,6 @@ public function findByWithAttributes(array $attributes = array(), array $criteri } /** - * TODO : allow grid integration, grid should directly use query builder * Apply a filter by attribute value * * @param QueryBuilder $qb query builder to update @@ -325,16 +262,17 @@ public function applyFilterByAttribute(QueryBuilder $qb, $attributeCode, $attrib $attributeCodes = array_keys($codeToAttribute); if (in_array($attributeCode, $attributeCodes)) { $attribute = $codeToAttribute[$attributeCode]; - $qb->addAttributeFilter($attribute, $operator, $attributeValue); + $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); + $fqb->addAttributeFilter($attribute, $operator, $attributeValue); } else { - $field = $this->entityAlias.'.'.$attributeCode; - $qb->andWhere($qb->prepareCriteriaCondition($field, $operator, $attributeValue)); + $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); + $field = current($qb->getRootAliases()).'.'.$attributeCode; + $qb->andWhere($fqb->prepareCriteriaCondition($field, $operator, $attributeValue)); } } /** - * TODO : allow grid integration, grid should directly use query builder * Apply a sort by attribute value * * @param QueryBuilder $qb query builder to update @@ -347,9 +285,10 @@ public function applySorterByAttribute(QueryBuilder $qb, $attributeCode, $direct $attributeCodes = array_keys($codeToAttribute); if (in_array($attributeCode, $attributeCodes)) { $attribute = $codeToAttribute[$attributeCode]; - $qb->addAttributeOrderBy($attribute, $direction); + $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); + $fqb->addAttributeOrderBy($attribute, $direction); } else { - $qb->addOrderBy($this->entityAlias.'.'.$attributeCode, $direction); + $qb->addOrderBy(current($qb->getRootAliases()).'.'.$attributeCode, $direction); } } From c48b2d678869cfadc3a1a8dc23ffff3e30a079a6 Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Fri, 26 Jul 2013 18:34:43 +0200 Subject: [PATCH 007/541] BAP-134 : fix unit test and refactor create flexible query builder --- .../Repository/FlexibleEntityRepository.php | 19 ++++++++---- .../Doctrine/ORM/FlexibleQueryBuilderTest.php | 7 ++++- .../FlexibleEntityRepositoryTest.php | 29 ------------------- .../ORM/Flexible/FlexibleEntityFilter.php | 8 ++--- 4 files changed, 23 insertions(+), 40 deletions(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php index a29f338cde9..fce0b0e4e9b 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/FlexibleEntityRepository.php @@ -158,6 +158,16 @@ public function getCodeToAttributes(array $attributeCodes) return $codeToAttribute; } + /** + * @param QueryBuilder $qb + * @param string $locale + * @param string $scope + */ + public function getFlexibleQueryBuilder($qb) + { + return new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); + } + /** * Find flexible attribute by code * @@ -262,13 +272,11 @@ public function applyFilterByAttribute(QueryBuilder $qb, $attributeCode, $attrib $attributeCodes = array_keys($codeToAttribute); if (in_array($attributeCode, $attributeCodes)) { $attribute = $codeToAttribute[$attributeCode]; - $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); - $fqb->addAttributeFilter($attribute, $operator, $attributeValue); + $this->getFlexibleQueryBuilder($qb)->addAttributeFilter($attribute, $operator, $attributeValue); } else { - $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); $field = current($qb->getRootAliases()).'.'.$attributeCode; - $qb->andWhere($fqb->prepareCriteriaCondition($field, $operator, $attributeValue)); + $qb->andWhere($this->getFlexibleQueryBuilder($qb)->prepareCriteriaCondition($field, $operator, $attributeValue)); } } @@ -285,8 +293,7 @@ public function applySorterByAttribute(QueryBuilder $qb, $attributeCode, $direct $attributeCodes = array_keys($codeToAttribute); if (in_array($attributeCode, $attributeCodes)) { $attribute = $codeToAttribute[$attributeCode]; - $fqb = new FlexibleQueryBuilder($qb, $this->getLocale(), $this->getScope()); - $fqb->addAttributeOrderBy($attribute, $direction); + $this->getFlexibleQueryBuilder($qb)->addAttributeOrderBy($attribute, $direction); } else { $qb->addOrderBy(current($qb->getRootAliases()).'.'.$attributeCode, $direction); } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Doctrine/ORM/FlexibleQueryBuilderTest.php b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Doctrine/ORM/FlexibleQueryBuilderTest.php index 73bf6e387ae..80e1827b79c 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Doctrine/ORM/FlexibleQueryBuilderTest.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Doctrine/ORM/FlexibleQueryBuilderTest.php @@ -1,6 +1,7 @@ queryBuilder = new FlexibleQueryBuilder($this->entityManager); + + $qb = new QueryBuilder($this->entityManager); + $this->queryBuilder = new FlexibleQueryBuilder($qb, 'en', 'ecommerce'); } /** @@ -98,6 +101,7 @@ public function testPrepareAttributeJoinConditionExceptionLocale() { $attribute = new Attribute(); $attribute->setTranslatable(true); + $this->queryBuilder->setLocale(null); $this->queryBuilder->prepareAttributeJoinCondition($attribute, 'alias'); } @@ -109,6 +113,7 @@ public function testPrepareAttributeJoinConditionExceptionScope() { $attribute = new Attribute(); $attribute->setScopable(true); + $this->queryBuilder->setScope(null); $this->queryBuilder->prepareAttributeJoinCondition($attribute, 'alias'); } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Repository/FlexibleEntityRepositoryTest.php b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Repository/FlexibleEntityRepositoryTest.php index d93474af792..ca860430aba 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Repository/FlexibleEntityRepositoryTest.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Repository/FlexibleEntityRepositoryTest.php @@ -86,33 +86,4 @@ public function testgetFlexibleConfig() $this->repository->setFlexibleConfig($this->flexibleConfig); $this->assertEquals($this->repository->getFlexibleConfig(), $this->flexibleConfig); } - - /** - * Test related method - */ - public function testcreateQueryBuilder() - { - // with lazy loading - // TODO : related to grid - //$qb = $this->repository->createQueryBuilder('MyFlexible'); - //$expectedSql = 'SELECT MyFlexible FROM Oro\Bundle\FlexibleEntityBundle\Tests\Unit\Entity\Demo\Flexible MyFlexible'; - //$this->assertEquals($expectedSql, $qb->getQuery()->getDql()); - - // without lazy loading with all values - $qb = $this->repository->createFlexibleQueryBuilder('MyFlexible'); - $expectedDql = 'SELECT MyFlexible, Value, Attribute, ValueOption, AttributeOptionValue' - .' FROM Oro\Bundle\FlexibleEntityBundle\Tests\Unit\Entity\Demo\Flexible MyFlexible' - .' LEFT JOIN MyFlexible.values Value LEFT JOIN Value.attribute Attribute' - .' LEFT JOIN Value.options ValueOption LEFT JOIN ValueOption.optionValues AttributeOptionValue'; - $this->assertEquals($expectedDql, $qb->getQuery()->getDql()); - - // without lazy loading with only values related to attribute codes - $qb = $this->repository->createFlexibleQueryBuilder('MyFlexible', array('name')); - $expectedDql = 'SELECT MyFlexible, Value, Attribute, ValueOption, AttributeOptionValue' - .' FROM Oro\Bundle\FlexibleEntityBundle\Tests\Unit\Entity\Demo\Flexible MyFlexible' - .' LEFT JOIN MyFlexible.values Value LEFT JOIN Value.attribute Attribute' - .' LEFT JOIN Value.options ValueOption LEFT JOIN ValueOption.optionValues AttributeOptionValue' - ." WHERE Attribute.code IN('name') OR Attribute.code IS NULL"; - $this->assertEquals($expectedDql, $qb->getQuery()->getDql()); - } } diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php index ea655f2b92f..31517a2be52 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php @@ -108,19 +108,19 @@ protected function getClassName() protected function applyFlexibleFilter(ProxyQueryInterface $proxyQuery, $field, $value, $operator) { $attribute = $this->getAttribute($field); - /** @var $qb FlexibleQueryBuilder */ - $qb = $proxyQuery->getQueryBuilder(); + $qb = $proxyQuery->getQueryBuilder(); + $fqb = $this->getFlexibleManager()->getFlexibleRepository()->getFlexibleQueryBuilder($qb); // inner join to value $joinAlias = 'filter'.$field; - $condition = $qb->prepareAttributeJoinCondition($attribute, $joinAlias); + $condition = $fqb->prepareAttributeJoinCondition($attribute, $joinAlias); $rootAlias = $qb->getRootAliases(); $qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then join to linked entity with filter on id $joinAliasEntity = 'filterentity'.$field; $backendField = sprintf('%s.id', $joinAliasEntity); - $condition = $qb->prepareCriteriaCondition($backendField, $operator, $value); + $condition = $fqb->prepareCriteriaCondition($backendField, $operator, $value); $qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); // filter is active since it's applied to the flexible repository From 0e99e2cda613d869e4ed391c59824158015c3014 Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Sat, 27 Jul 2013 11:54:14 +0200 Subject: [PATCH 008/541] BAP-134 : refactor flexible entity filter and fix unit tests --- .../AttributeType/AbstractAttributeType.php | 27 ++++++++--------- .../Doctrine/ORM/FlexibleQueryBuilder.php | 13 +++++++++ .../ORM/Flexible/FlexibleEntityFilter.php | 25 ---------------- .../ORM/Flexible/FlexibleEntityFilterTest.php | 29 +++++++------------ .../Flexible/FlexibleOptionsFilterTest.php | 15 ++++++---- 5 files changed, 47 insertions(+), 62 deletions(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php b/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php index b00ea5d52b4..973fe03576a 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php @@ -27,19 +27,20 @@ abstract class AbstractAttributeType implements AttributeTypeInterface * * @var string */ - const BACKEND_TYPE_DATE = 'date'; - const BACKEND_TYPE_DATETIME = 'datetime'; - const BACKEND_TYPE_DECIMAL = 'decimal'; - const BACKEND_TYPE_BOOLEAN = 'boolean'; - const BACKEND_TYPE_INTEGER = 'integer'; - const BACKEND_TYPE_OPTIONS = 'options'; - const BACKEND_TYPE_OPTION = 'option'; - const BACKEND_TYPE_TEXT = 'text'; - const BACKEND_TYPE_VARCHAR = 'varchar'; - const BACKEND_TYPE_MEDIA = 'media'; - const BACKEND_TYPE_METRIC = 'metric'; - const BACKEND_TYPE_PRICE = 'price'; - const BACKEND_TYPE_COLLECTION = 'collections'; + const BACKEND_TYPE_DATE = 'date'; + const BACKEND_TYPE_DATETIME = 'datetime'; + const BACKEND_TYPE_DECIMAL = 'decimal'; + const BACKEND_TYPE_BOOLEAN = 'boolean'; + const BACKEND_TYPE_INTEGER = 'integer'; + const BACKEND_TYPE_OPTIONS = 'options'; + const BACKEND_TYPE_OPTION = 'option'; + const BACKEND_TYPE_TEXT = 'text'; + const BACKEND_TYPE_VARCHAR = 'varchar'; + const BACKEND_TYPE_MEDIA = 'media'; + const BACKEND_TYPE_METRIC = 'metric'; + const BACKEND_TYPE_PRICE = 'price'; + const BACKEND_TYPE_COLLECTION = 'collections'; + const BACKEND_TYPE_ENTITY = 'entity'; /** * Field backend type, "varchar" by default, the doctrine mapping field, getter / setter to use for binding diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php index 2fc1e57ae41..1804dc54ec6 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php @@ -290,6 +290,19 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); $this->qb->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + } else if ($attribute->getBackendType() == AbstractAttributeType::BACKEND_TYPE_ENTITY) { + + // inner join to value + $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); + $rootAlias = $this->qb->getRootAliases(); + $this->qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + + // then join to linked entity with filter on id + $joinAliasOpt = 'filterentity'.$attribute->getCode().$this->aliasCounter; + $backendField = sprintf('%s.id', $joinAliasEntity); + $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); + $this->qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); + } else { // inner join with condition on backend value diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php index 31517a2be52..057d003b5af 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleEntityFilter.php @@ -102,31 +102,6 @@ protected function getClassName() return $valueMetadata->getAssociationTargetClass($this->getOption('backend_type')); } - /** - * {@inheritdoc} - */ - protected function applyFlexibleFilter(ProxyQueryInterface $proxyQuery, $field, $value, $operator) - { - $attribute = $this->getAttribute($field); - $qb = $proxyQuery->getQueryBuilder(); - $fqb = $this->getFlexibleManager()->getFlexibleRepository()->getFlexibleQueryBuilder($qb); - - // inner join to value - $joinAlias = 'filter'.$field; - $condition = $fqb->prepareAttributeJoinCondition($attribute, $joinAlias); - $rootAlias = $qb->getRootAliases(); - $qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - - // then join to linked entity with filter on id - $joinAliasEntity = 'filterentity'.$field; - $backendField = sprintf('%s.id', $joinAliasEntity); - $condition = $fqb->prepareCriteriaCondition($backendField, $operator, $value); - $qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); - - // filter is active since it's applied to the flexible repository - $this->active = true; - } - /** * Extract collection ids * diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleEntityFilterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleEntityFilterTest.php index 5869fbc9efa..1b590f5d02b 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleEntityFilterTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleEntityFilterTest.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\GridBundle\Tests\Unit\Filter\ORM\Flexible; +use Doctrine\ORM\QueryBuilder; use Doctrine\Common\Collections\ArrayCollection; use Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository; use Oro\Bundle\FilterBundle\Form\Type\Filter\ChoiceFilterType; @@ -41,22 +42,6 @@ protected function createTestFilter($flexibleRegistry) return $flexibleEntityFilter; } - /** - * {@inheritdoc} - */ - protected function createQueryBuilder() - { - $qb = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Doctrine\ORM\FlexibleQueryBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $qb->expects($this->any()) - ->method('prepareAttributeJoinCondition') - ->will($this->returnValue(self::TEST_JOIN_CONDITION)); - - return $qb; - } - /** * {@inheritdoc} */ @@ -65,15 +50,21 @@ public function filterDataProvider() return array( 'correct_equals' => array( 'data' => array('value' => $this->createEntities(2), 'type' => ChoiceFilterType::TYPE_CONTAINS), - 'expectRepositoryCalls' => array() + 'expectRepositoryCalls' => array( + array('applyFilterByAttribute', array(self::TEST_FIELD, array(0 => 0, 1 => 1), 'IN'), null) + ) ), 'with_type_null' => array( 'data' => array('value' => $this->createEntities(2), 'type' => null), - 'expectRepositoryCalls' => array() + 'expectRepositoryCalls' => array( + array('applyFilterByAttribute', array(self::TEST_FIELD, array(0 => 0, 1 => 1), 'IN'), null) + ) ), 'with_type_not_contains' => array( 'data' => array('value' => $this->createEntities(2), 'type' => ChoiceFilterType::TYPE_NOT_CONTAINS), - 'expectRepositoryCalls' => array() + 'expectRepositoryCalls' => array( + array('applyFilterByAttribute', array(self::TEST_FIELD, array(0 => 0, 1 => 1), 'NOT IN'), null) + ) ) ); } diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php index 4060807e255..e752a698533 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\GridBundle\Tests\Unit\Filter\ORM\Flexible; +use Oro\Bundle\FlexibleEntityBundle\Entity\Attribute; + use Oro\Bundle\FlexibleEntityBundle\Entity\AttributeOption; use Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityAttributeOptionValue; @@ -70,14 +72,17 @@ public function testGetRenderSettings(array $flexibleOptionsData, array $expecte { $this->initializeFlexibleFilter($this->model); + $attribute = new Attribute(); $attributeRepository = $this->getMock('Doctrine\Common\Persistence\ObjectRepository'); $attributeRepository->expects($this->once())->method('findOneBy') ->with(array('entityType' => self::TEST_FLEXIBLE_NAME, 'code' => self::TEST_FIELD)) - ->will($this->returnValue(self::TEST_ATTRIBUTE)); + ->will($this->returnValue($attribute)); - $optionsRepository = $this->getMock('Doctrine\Common\Persistence\ObjectRepository'); - $optionsRepository->expects($this->once())->method('findBy') - ->with(array('attribute' => self::TEST_ATTRIBUTE)) + $optionsRepository = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Entity\Repository\AttributeOptionRepository') + ->disableOriginalConstructor() + ->getMock(); + $optionsRepository->expects($this->once())->method('findAllForAttributeWithValues') + ->with($attribute) ->will($this->returnValue($this->createFlexibleOptions($flexibleOptionsData))); $this->flexibleManager->expects($this->once()) @@ -137,4 +142,4 @@ public function testGetRenderSettingsFailsWhenAttributeIsNotFound() $this->model->getRenderSettings(); } -} +} \ No newline at end of file From c1f48c2e801db55d46ad90e3a567ef12e741d418 Mon Sep 17 00:00:00 2001 From: vladimir Date: Sun, 28 Jul 2013 17:50:49 +0300 Subject: [PATCH 009/541] CRM-287:New design for View Contact page -add static style --- .../Resources/public/css/less/oro.less | 233 +++++++++++++++++- .../Resources/public/img/general-sprite.png | Bin 9678 -> 10730 bytes .../UIBundle/Resources/public/img/map.png | Bin 0 -> 45031 bytes .../public/img/sprite-horizontal.png | Bin 0 -> 2056 bytes 4 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/img/map.png create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index bff54e37f51..e7e1be2e9f0 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -1042,8 +1042,10 @@ footer{ } .btn-mini{ height: 20px; - line-height: 20px; + line-height: 18px; font-size: 11px; + text-shadow: none; + font-family: Arial, Helvetica, sans-serif; } .btn:hover, .btn:focus { @@ -1058,6 +1060,9 @@ footer{ .btn{ #gradient > .vertical(#ffffff, #E6E6E6); } +.btn-uppercase{ + text-transform: uppercase; +} .btn-primary{ #gradient > .vertical(#739cdc, #4875bc); border: 1px solid; @@ -1610,6 +1615,147 @@ footer{ .icon-star.icon-gold{ background-position: 0 -188px; } +.icon-twitter-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -473px; + width: 13px; + height: 12px; + margin-top: 1px; +} +.icon-twitter-new:hover{ + background-position: -13px -473px; +} +.icon-facebook-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -485px; + width: 9px; + height: 14px; +} +.icon-facebook-new:hover{ + background-position: -9px -485px; +} +.icon-google-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -499px; + width: 13px; + height: 14px; + margin-top: 1px; +} +.icon-google-new:hover{ + background-position: -14px -499px; +} +.icon-linkedin-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -513px; + width: 14px; + height: 14px; +} +.icon-linkedin-new:hover{ + background-position: -14px -513px; +} +.icon-mail-new, +.icon-phone-new{ + background: url("../../img/sprite-horizontal.png") no-repeat; + width:33px; + height:32px; +} +.icon-mail-new{ + background-position: -33px 0; +} + +.new-line{ + clear: both; + display: block; + width: 100%; +} + +.list-inline{ + list-style: none; + padding-left: 0; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +.list-group{ + +} +.list-group > .list-group-item{ + padding: 10px; + border-top: 1px solid #efefef; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} +.list-group > .list-group-item:after{ + content: ''; + clear: both; + display: block; +} +.list-group > .list-group-item:first-child{ + border: 0; +} +.nav-tabs-content{ + background-color: #f8f8f8; + padding: 4px 15px 0; + margin-bottom: 8px; +} +.tab-content-small{ + table.table:last-child{ + margin: 0; + } + .table td{ + padding: 5px 20px; + } + table.table thead th:last-child, + table.table thead th:first-child, + table.table thead th{ + padding: 3px 20px; + text-transform: uppercase; + font:bold 11px/12px Arial, Helvetica, sans-serif; + border: solid #dfdfdf; + border-width: 1px 0; + } +} +.nav-tabs-content > li{ + line-height: 16px; + height: 22px; + margin-right: -3px; +} +.nav-tabs-content > li > a{ + background: none; + border-right: 1px solid #e4e4e4; + padding: 1px 10px 0; + color: #666; +} +.nav-tabs-content > li:last-child > a{ + border-right: 0; +} + +.nav-tabs-content > li.active:focus > a:hover, +.nav-tabs-content > li.active:focus > a:focus, +.nav-tabs-content > li.active:hover > a:focus, +.nav-tabs-content > li.active:hover > a, +.nav-tabs-content > li:hover > a, +.nav-tabs-content > li.active > a:focus, +.nav-tabs-content > li.active > a:hover, +.nav-tabs-content > li.active > a{ + background-color: #fff; + border: solid #ddd; + border-width: 1px 1px 0; + padding-bottom: 1px; + color: #444; + font-weight: bold; +} + +.nav-tabs-content > li:hover > a{ + font-weight: normal; +} +.nav-tabs-content > li:hover > a:focus, +.nav-tabs-content > li > a:focus{ + font-weight: normal; + background: none; + border: 0; + border-right: 1px solid #e4e4e4; +} .badge-enabled, .badge-disabled{ padding: 3px 10px 3px 5px; @@ -4091,4 +4237,87 @@ button.btn.minimize-button i.icon-minimize{ } button.btn.minimize-button i.icon-minimize-active{ background-position: 0 -456px; -} \ No newline at end of file +} +.small-well{ + margin: 0 0 25px; + padding: 13px; +} +.box-type1{ + border: solid #eaeaea; + border-width: 1px 2px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background-color: #fff; + .title{ + font:bold 14px/18px 'helveticaneue bold', Arial, Helvetica, sans-serif; + padding: 13px 20px; + border-bottom: 1px solid #eaeaea; + } + .box-content{ + margin: 20px 15px; + background: url("../../img/vertical-separator.png") repeat-y 50% 0; + } + .list-inline{ + margin-left: 0; + } +} +.well-small-bottom{ + margin-bottom: 10px; +} +.left-holder{ + padding: 0 0 0 15px; + float: left; + line-height: 16px; + .date{ + display: block; + clear: both; + } +} +.dark-bg{ + background-color: #f1f1f1; +} +.map-box{ + background: url("../../img/vertical-separator.png") repeat-y 40% 0; + &:after{ + content: ""; + clear: both; + display: block; + } + .map-addrres-list{ + width: 40%; + background-color: #fbfbfb; + float: left; + + } + .map-visual{ + float: right; + padding: 20px 0 0; + width: 58%; + border: 1px solid #bbb; + } + .map-item{ + padding: 15px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + width: 100%; + } + .map-item.active{ + background-color: #ecf1f9; + } + .title-item{ + font: bold 14px/18px Arial, Helvetica, sans-serif; + float: left; + } + .map-actions{ + float: right; + .btn{ + border: 0; + background: 0; + -webkit-box-shadow: none; + box-shadow: none; + padding: 0 6px; + } + } +} diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/general-sprite.png b/src/Oro/Bundle/UIBundle/Resources/public/img/general-sprite.png index d2815a93c1766f2c2ef0be7bbdd186dfd080aeec..80d4b52da954a4168bb1c55eb506cbf345001ce0 100644 GIT binary patch delta 10144 zcmZ`s%u^8flvlar=7 zSDG|ECvC4zE1qg3;2}eyBfvpGK$y(-Ml@2=LK%GKlaZ9>0KWEGX?{?(Xo-5hO1yLiNwWYGGp`_&+S?e_@l0pxFQCfL-Rl zE>Tgqfz-cQ3R?WP1NQ&ICbL3I0$J?gY7h`~M)J}UnqF(?eu!Q;pVvOtlQWe#Gc7nn zV9cNpBT0fNFwi>pF`RG2RTCd%-e9iGKbsojcE2-K`|nmo-}Ycv=5SYjKk&2v|4?(WOKE z$?py!BW!a(7(~$#uLbjov=Tju>;z?Af~HGd!git>bvz+yoCkIOLT)|b{L74HWD5?X zY$Z#5Hv~j;g1_^Nigw9%2y#ZO<|PVi4-OVZ_1oAAA)!nk)XL0MvdV-LHAQ8o6f_-Lt4-tBxG7jN=zDbYg^lr{d${&TE3{C zj7g$m!fm3UTTjW}yRq-h(4KBx9Q1qey}?$mw!Cqo(mYCy0Wa}_G4OtyzTjnhU?D-( zCTKl65=)#eYSJGN-KtlcsKZ4)MA`b>!VMGgLp3=){RnTxbIaRm>-Ru#=ukBNhmL}R zf^n>jxumTW(G++=rGA-h@XiRUP^jEm5LcJTb0-wPY)^Ml)jT#f7G6P7(U`Jsyu+Z& z{cK74)h{zlTNH+$8JIEEj|_~V+|)WfSt!?Wp{&%x^kb?ThUBUBetTH9slkSVEaBib z3yoZ87F`dBBAk~ z46*E#GBY!|>!`y}iFrQ>2s}gmYM0gwSAY?0uwQLbR!n1N9C3}6Yc0vAaZ*5A!7pHF z{G~x%4pY}va0B%D z0Q4EJAGx4OxhA&oTeiULnEfqqEkJbgfXDPUMd+9>un8TL^rbpDJ>QR{Ou2Id zIcDv-&>8=cRkal(rva%HgPt=X2C5(h012{yCdM_p+gv98sJhf;L6!CY3YxFvN^JlQ zg-di5j=O1H_c4i?vCb6si{Oew(yBrm*Q_Iu`+w&(X=uhxW&e2oJQ<8Y1ep@1xafrp zwMBip7KJVvC>5@#isTf?N_bL$2X#av6#9I+czFzG@FTJppD3>X zvH92JsIKQCv7|pE{(PZhV3gp=R%3#W#KS52cOJv8JE|Vu_eYI2*hgwN|JRh0)8wVN|<&vglHN zhNvsKXk|6F`z5^cF9bF#^~wA+0k)*J$!nrUdXq&?ETP+z;_0NOF6P`=4@ypQmW92} zuTM9~9Bj+7&&8{&tF(y*cV&_LNF~TPb@`G+WG5$Uh$UxF%-UHs!XMJyz+pHJ`37~D z2l`C>S_p`CzWVh^%b)Dw2=6ZOQ^LEDZE{vzxgBW$X~2g!I`doU?ygx@VO*I2`aDO; z75L$F5zl2vmI6r|k4ofAJUmVM7b=mMf_#~*()}NKX#uNGPI&X5x(nB-;38rT6}fEu zUqsc>;K_no((C_rhaSvR13Y1#cgA{!UC68Sq86;u7`K_9eNFw~=JTlr=A)mkY!L|* zy}s;>IQp;AtDCQUs`ITJlHb*nDuEb!NL)_{wQ0KPfdb&h#zV{TR;+>7)gk;9bjqFx z%_wf96T%XU61uEDs%VzP`N7qv=AY;~?_r>7-yErVDSHdXcZ0ZlfNe}yt=@X+g+uIv zXWqCYF~T4s5H$%c>zZy_t_I`eXsUZuwxnG_Bvx0c7~(q&LSg z$lxU99x@vr*^Cv)CxAP`*48%ZXs&rH^0$xPRN00casPw(YHm_e(vP?`YxkY6C9(-9 zO|JWpTjD->Dsxi#KphWn6LI;hlOe>NcNS*yM>TS5_%4Az1EkO(eHnEk9Yqf{qgYN? zK|@HG(YFk$yUXY&GpL7S(Wq}U%Rnedi$9>*!h3;VRp8HI3flPiBpSSP%<>N+h#nT# z05fHOQ6p_jYyJR2q-SoO4!9HU^l8kdGn*XU(+@*S^4Hql7E@{iaKgn!_DD|3F{3Ft5x&-(?w9=omdIZUyRS9veVKLW%#zqbY0y>LHh;|HSroxA>f@~l1)z}HUPW0wL<(G>%n7k(8gt= z-cZ44=I*#XAo_MX(=(`>&=X6@YC>9LurbE<+;~-L)GpwoAM$yzVbndA*5Z zs`pNaJlc^e!J0+}KU$i1TvoR__4GWYhUa3QQb8Wta%Uyh_dZ`T!(`I*$o+OB&uZTI zSE79ex|`%0?+JL1eQy*0rt|UglHjw=avTU4IQ=1sE=dt|*?7MR=X>X#uvYTu^Zq=N z;R$aTjL>_a5bz`S&&Egh=od3;tk4+oJ-OwQc@jjY#OD3@dWW;cqoQ69C2mU)Usz^FrK;`Jgn%)T+)x}%gIxb@!t=oHs7 zJPtv#@7!>*;YJj$9plaE;I*G_jV!3u(%bAqL#xML+i_FcH@;Vo%C%->RgqJlU1EL$ z!k^BdEVm_A4fk$IOe*s=V{1Lk}dF*ORF)e)bM32 zW79mF8ERWySBybURNN+ z&enBHE>GJS)BZgBJ`VZaeT;vh)-Ih0@ZR*wWvx}Dzx!L^q*qG3@M1O2AtXKbn<~cxB4Wk(u{1*Eag_FJ|RwWYk-DLSu2Qu*_<3^Yze{e@)NDk|OZ7#O( zz{JQdkTfI>F$(s9gWWHNE(>Aa&{e*ke`u3fTb9!JlmzI`phmP`RW-ga%si>(3C-Lob2Vy7# z)rTDU-h2*2zBXU_44u5}!iwLbhI%=+>al2)e<_7uxF-|q@%{>2r2x@*{To4BlhwRL z$!9-j)ERiVxitkz(X0$|w5{YgGhp$fEBUMj9yWVKQaDN!aBQ_QG&G1y^7AMC%r3hL z`L>>B$5WZMO!($5mzbW%`)F5i(OgzT@@(5}-35h=n|`Z-gg-A?shtq#u!iDvZVY#t zq2sB#$5RruYbFDcTiX1IX=!PDyz_GzS8G}_|i*evAXjYB)>l*+X9FRu>NZrFsQN0JB=;Jzh|!3t&Bn2 zI6VH#EX&Pg(UD}UqSq_JZPf8%X`JF+gLMiwh=>gzs*JB51JO}A7z4K1HTKlhrM%r> zIem-gg8dZD74UBa-}XiDQKFXJlBBW}G7SxZ<{48=;fMPY{LuztdMw#aDlo#rHLwPp zhQxlX5p|HQy~&-_WY>pR)5WK+$o)gmiRZ`+4uc+RjJXWA8occae5`d!H!i;-@J<^R z7B*rY3g7cj0x#Yq`5qGA9S*tWIB6K#FjG=4#hft?-#sAha{4d9Ply#2HUU3+;7#AJ z^4yMQdOtWS>M9?YN+mt~jgR^%sR7@n!p1@}){}MYPt{e0MOA34Cog&pg}rES=p6`O z@0s;qkeI00@-e+8Br5_ypYT8)1;->&%AT1F} zA)6m;@@Q;p3pbZ5v<#OL&O+IP@erL7NJSf+{F$$M2$0UmnCrxz?DZ@A`+C2spnKE4 zIBV(D{=V7FxwAy5vAKRkD&|e3?b|~4GXsuz`osgWIAx#4%l$^*q;*wzoaCvKK=kD= zk!RyCNv<;xbpS6z>p`PAXyZ0m zH$#?;SVVi{g__GcHW7X65A|}?s!{98ZvF1wM?y@WynBp^lhC_MF^Sh<&U}uA` z8+K0gkzWSyTMPvF`~Ak#d_B=o39=}LyzF%k+h$kOQmEx+GKhEUyD^t(cq@=6x&rUZ zx83oV=6hU)${vcsI(5HbZTReQcf3R)I@i307 zh^3PR!?L{xrYiE3!f;bQU?Q|D%IfLScQ{RpOa?1XSXf&AIPY>w|Gv==wG$2RbEPXz z&O~#(7M#D2gMDg%z3$z`VN-xKRk*5TtOqunWs zefk$07>~9hB;cX$4bFN`GdEwhTa>in^||3yg^%0bYRRjRqQu(iKejl(lqgQs;$lVs+uR>bO-B2wz#u zkmNoz8ecCyH$syLQ0`xJQ`B4K|AutC1AiXz(#WwH`K>+s^YEmGm+Z*Py;aMEvwR4S zonsCH4**HgpBCYN@(GC?-p4`NamT1^&q>7!Eiy7S`%r{ygblGS;Q>B=3MtjpR0Uf1 zq*HEw;-E7v0$sYki-F>vp&Z1~nGZm0BE85(zAGXmf)&810b*vUDI^>bT3NZfS^WJ^ z!IRvZR|EI^VdsBuM@r}2Nig&2?@wsYaKW%Z;Ng04c{{`^&nJlVQHr1}gdB8SXW=~tB+0>Bx^H?v<+ur<^H zq0cM{R4DcAH=1vA|C*l4!^gKVK_lx6i1U9Uj1DQ46}Ys#ETmJT3(3OL%U3|oWrcnJ04cEI{@!m@SZiUVpr*RKkexue7inw!{@tf&)&7wQLs?mQN~So` z<40LJMT^T9MT#y)8-9v6b<=9P{s$s5qIs~jhrAGE=nKy0xnjaLHB2ZUX{|1g3)icY z#d_nu$)_hTQg~35$esf~5Cn;{=Z@+$nhS?c)}5V>nXp-jCH`U)`Zz^G#hcl99oCwK zz#IT)#=R- zf@xbg5xdy`_6Fs{*qeeY7wRRNk-wZP@MRdt|eBr&s&SsC;VoIAC9VNi3*+m9;v!{B##cNm7;#*K-gl6I$ ze4YE-F0UO;3}ZY4i>q7peBM0sJ(}6p&aRkUt(-Ts%uZ8=d1kc+(GsQ78L8<6IWlFd z94=xx4{Ksac(1+?0!t{#<9vnVbJePWvhcWqR`~p~F`!HyN-U{{oYWuFY&FBG&i?I8 zz&legr)>_vv%4vT7V|+Ri`w%en(6|bNVwfhjf!|_wT)ItU-S2ze_&LX0ETG?J->JQ{@GJpF zMpIY{-Lh2L!=umE(3uXja6D})Dd3mbV#w+=*+>R7wGc9Tp;Y!uj?fGS0S#$Y_4t1? zwzUTd;s!7hhY!26qxG|94lj=*s>dUawe4Qp;=sCtRu-;oQ@HwH<~l zWel+Z4zFJybpekP8xfyv1vtmZ_ral}veG>NWWK%s{*abbV~hBz#+i@T|6Vme9tflf zKtoqhQT(>9sk&y=0dISX!E7|*u~wK1d#qEv(A0gKO#hWo;g^O~(N?ucq9!;a&(dSt z{qr~6APUu-r9t<(KTj$6_~pUHBO4#mW5WwN=(r=Q{&kq#Rd@c&MGk*sxp}|(R%PNV zp5SgIH9dZek{CH7{vBlC;hp38L=BYfQq$I;<};V7?38M1=%b6BgB)_!sp*O7jCkj( zW8{|KPhGXcupIhIwZFV>7!_j_HnzM>JY4R99kT;bRj!G!$Ge_is=v9zEQY7spS$@y zx}%Zq-2mk=v=*yYU;2m~J58&8rhHAiiTn=s}3}5Uzq!OTXuq0WojEh=>&3b#FM%j^!W;d0uJU z$Ci)Be}~}edQi=+A>#)TcogK#@p80OMsbWSR;z19?!xBtg;e8=j)*v|;> z2h6MJ^HpZ^;-xLbLAmHTnu5+VZh!iYFPH{MFaIGIhCwzmn$%UY5#Qd5k{r}s;}IIM z`Za_gv8zhJ?c%cW1H&W-IS1azYFb--ZYKQevr69+T$Qb@aLkWP>=6L!&(mbh*gKXC zk$3~~fJa67-zfh>UOuvYbhj0|W&ZXc=X|EQS_FjgFr3sJhwI-+7b25IX+gh+U-aa7 zGv9bmZAc7M@L=A7IMzYd4v^~Yvbota+)s+-vcia`r@29UY<8hl^+!+t@qL^RP;_G0 zn_#3AlvhgPp<4E@#~WbY4A0Nb-M73E`t+h5Q{J$CB_wABUK9-zutn~TxpdK9?_q&4vIolAU9s>6Xh1f^>D2n)7NMdNOG z*p0TsQc$4xiExIU6uqlGzDDk6gDSgQtDZhe8u1j#m*v=a9s>L01oWPw^uypX@Bz5HOAB^#a#o46@d5m!iH z^og2gO%4QBX6J$N;$Ef@=&}x@C=r$uQG{EN4{QTcz__}u0w2D7t166nVHM{D&N?PmtYqut;UyAo8OO)yU)UDNv2;nMpQ%l7@U2-76p19CkS0|kxJ zn@qL!^~RvX$;_;(Pydo9=D_ERKRs$^vEoKdu4?`KSj)-&F5JPCM%wT z>>Xn!Gj$-3<#T90DdvK=$UrM~`zvNg0C(cJ-tR^L6?i@`S+5#9V zEdfUm~&Dol4Zg7@+}gN%<`vc z|CoHnno)XOB=`iS^`HCu7XN;CQ<}#pX#xk^ziV_vu-v;`UKF@^zv)*R!XqWV0i8`r zhbk0*PKl|^*7gxcszcUirw-Wgih#`I*j7Nv3|YM@rEyM z{5Ve01)ehnL)vCs!N?#Mk$u6q>EeY`vqb2qkzWuQ7HVY73bsQBKg;Ylq zl?CV4q?gl_v-2bbsVT_G6=5mXdbfNxpY*)2$r{RPC)Q_iz;{S+ zzKMo=qQa#_6LFS&&ZhsYOX8$p3Qjqle0P3yxZ%F zA$%ZI?cSv+q1g0iJ%~CW5+E;E^+s-LP9ujmfM5N}1V4%D++B(*Rk;V(t#J}0ne<+$ z$KbvZ5D3#Tk(m1}=`P@uLTI8J!dq(3k6ZFNek@HdsBSM!J!R77Nl5#fLB+0DWJAK! zk!${ObDV*0i7h2>!Y4v&tfQF}6sW#BJ=rmeCmx3*t&ED)3@)=7Tc8q(S`jw0_WqO9 zU#5_?!DX!)SeUPLCIJ7Jsdw0aO20wm)JBcEsE!#mU-&7y){rKNlrgb$2v-qiBzFcO1QU|wE%|XlT$TG~X;ySWWS^>}*(8uh=5gqQ^nWX9rtCt;Gbv}S# zP>JU1P^gmT%8s-El)GIivsT~@_&h=0RRv!>AcT~9I5n{A$_FYs49MDY&vkF{x1#ygUpBPAH*blD$HXq!ADLDWJx5 zPt{~oRMAkV5=IEjf{_z#`J5#;VcKFYMl?s#=o9XqCH$+*|BAODeq| zILCeo%?gqMwj6QM5By`oJE-Jy*$m7^?ysNS`Vw**I`sGxJzx)%46Ame*KqC8M{c(R z=O3~llDhIhQ*;;rgTj*Ii9KJyeqA~5-UCnj3YFV%r@^bI8#r@ff;oNj7hYz(X00q= zX=(@7+;7&DoMnZSoO$j#r#-3KBNYV=TEWA-CV3QDY1M~OIhZ0mPgk}^-GP?pnh}vu z*&cC-p{^Qa^#h!>f@5)@;g^K*6A)Ra(gd*|(H``$}c0mSg{g~!IxM&kdpg8xmM zR)WCyp9FmWEr@~6(@W3RPQvEDC*c2Y+B6TI_ta z``%YZ9~N3enGPtsP_Yy!G1v&yc=zQAzusd9Oi-K@FY< zlYq&<{s%d(G#u-^E;wQB0RLz@w1Q0zgwN4Jcw-;E;E&K!<*Vry_w`Y2RaEzLaE@@w zEL3nZeZMG}#-x`TWNN>~3(Zz}^+MM2DJxXM$ci#J!{7!{lF$j4&L6FvVlaApc}=2| zH`dj;hV`jY<0xNe3)+itT6{+ZnV6eX(E=Dnq1v6SaHQC8-Y-{Z=F$wt)TCQBG&J<_ zIP`-8Z*L5emGG;YN=kOV%E#dJhAKYNTS)(_TlMf~{-cU%eR;VpH^@&Xw7k5Wa8LI1 z^i;Up=hRf#eXGCe&7NABRN0w1@6lZ{w^ZflAa6X1V- zx$Bcjs`SyCQdy|g`g=8&##)I@Epda<`_}ECeInl*0ztv5YHVr>igr@d@U2To zM#l2ZSGy^IiIo+1?#Roi45th|FLywx)B{v-mx;E{tTq?XLacldEYN0usID!JDZgRkrjsfFJ9ujE28!<-&sB&`%^(5tm?`oPs z5e$PSHR6`>=#tz27c%RjxvG`lO}3zkJ9_heVnapXHdDLVRaMFK!Q#z-e*XYMpa{&osnjLKHO4xL(Q~rCW6CDLpBaG7q0SuLP&|NLEh3g48r}wBPqGM3=7=LA zSBuVrSPyssODMr^Bk-1I&GV2uIeYfICoQxF^Kk_|8~~MV?%PkxBZ?FcTSqsi?ej6b zs&zn7MfoOpPt`%v&uc4!R=*bYgT&aS1I!cTpO5J!M_C^ajUSiKZ(7dWM|;HILejAU z5Npl9k=ac@72uLBUQd?<|FCFuqVL$EP6t!{2t@{vW!cHG0)G6Q)akiBIdgw4uZeXK*7JRf179Ojuf^@F$|uNWW!%fXJ5ny(!YPEPliBO!JI)r67)b z?YarM1>sug5b+{&$E<;T2ne}45!woAS_Ph>WzR5X_y3ruT1COV*Jt;0si3gO{%G}4 zH2f38&OGZUxnK)yF>|eoTBp4d2~`-`(ekV!H&f_ekOAvra93@l;${FwUv`+Zf8KE7 ztjbXKtc_LiH+R_0(F9*ZQsEVAb91v=YuWO44zkStNmJ z8N$R%`e9VxiTd@a>rxTA^^Yw%uWzTOOJ6pu$@P%u07X}wd8khvW+Uds=6RHDk%68b zm~jDJ=s->)>c;l<Oo@gUUYn@$G)bWSk5C!c3saJjW>5k5pz3YaQ$&eaY;JAD%qTbwGIRnG#Z} znD+K(QZ5sOYg;^ucJpFB%oJ+XEp}Z_!W!#|Z3-WE&Dj2quwoo`=Ol%d`8i&LyK#=* zZ+Vn;If(%jVGu1ubA-->sYjS-*%X`GI`RzQ=#83jaDh#ESa(^& zM^1{?m+Zn0s!6Bah*HSqHef>14qz$MR;@xj2kv*kVoHNq#g7oxsB98^7 zub2}lG+=?T@f1`){3CX01ppW5__h3lCJ)?KCzo*OsG#xEz+;XxEFGke;1hM2-C1t@{LhmSfoOo0n2 zPS#fIi^eripEFK@`&OL%Dn6&3TwvIF-p2^P7Jk#jRk(5Fbr?b75991(wN}4I&(s1O zap$nOxv+aZ@*-OZsuhdPF-1|*{8m1?RtuRJ^e9Go`c0uhn!}6DVLu zlCdB>sn=G1BVl-s$Q4kl>_k;fLMbx9CA7geLGf+T_d^qqt+)r5h4DQ|OX8Nac}!fv z&wcMHMD;k=ttXBNw>b%+T66ALq`AV`yTInw$W=dx)c&$g22{7}s0mv0XQf0(uUH!@VMTK$q zyq98W89W8O6TFo2NjDhuHtB<}#cdC1dDu7pC3RP%&PamZn50!n*$yjTBBA@?b)<-3 zdDUMcnx}QB1q?}68>STuVpt?ed!17zCSq3(W8UOm<((~pDqGU^NN;~z0?-{dlB_9` z>2Xe2qQ6XubI~@3F#^$9&*Mhr&_S7a);e<31QAl_C z&(>z4S6q?|OMdIr#r<83&!!vuP%}Iv7@zW{3AhNEFW;B#Evo5lne#z#*>|wkgKp2k zGbps5bI}_OeE6H}gxXYg&nweDK2T!Fvx$egJNBDZT)cDXChA>~9qBag$;K?tL2*t? zC;O)qAWPYHz5hhAO5~2eUXyG3r~i7ww?}ZPPcdY0F?{)*w#A6E(TB*K8BPrUIlyZmCtpV+AL_A?v(#*98r@;c))*k_ss5WWiB#g1N}S$FA^51!`)Wo=l_uk~Gs zjCNd>7B%n8b+Q(i1i!O*Hy?Ns+d#Hw{&bPF!mvpC_Q~yVBVzbupw904q%!1X-Z$_@ zCBmZncH{Q=b*0Gu_^2%SkDeogr}fT9k6mSf>y(TFjAzfsZ=4yqeD9ge7*vjlSwB(k z1D$=A?{N?r2}SK(;NNIeI9_#mJLRu0teSdl_2ai4G&VJ%kB@81@Mf^D`r;v-b@z$u zoOD*6*gRO9;%ZfQbRbdWn6*8;vDU*BZW8$e9qU}lZYAY0TsK;pxmRwGK3OTnDja?j z@?u?Kpp!bK`c-hQt1ob8va_Y__Ld_P2}lO*xFZpTC%5T%8;r$&b(@RiyU{=^?opr0 z=py35z|cO?n>8~|Rsyry8efzyhlBOI48&L`7~J&azL2P$V8X-ZEd@zc5`?Q(^fJTA zJ(0JDEaz)LEn0PB<``4>KR%(PzV#Sxu)HJqf!ztd;gb-iqnG`PV)m1`BsZeY3}{C& zkSuSATTmf|bKxRcjTo)1Le7z&V0mOb-u$ek)za3YQ2?^n>NIcw zAK(qJ{DlVWVNI&(vEk6*^|*n-z>fX7>uNfoPa46Spr9vZGJ!0)r^sT#WCVVOTefdF za)gAwk%;5p#=iNzfY++8^zXF9zTo%i?SaWxyV-b0(@||sUf|f`%565B z%xq@7$LZ^e9&1PjxVpHwcy8aTYiH*MML%_nr6V`1qZ94y{ET}0hJIb4*eHsdxm*tR zFqW#E!Id9)J%#JC*0HU4iqT;|W!Aoc)rX-Su)$ETUIC0p;@$xIb>~h9P za!N`{T5765ZCx+-FqvDA*VV!pXi9U+FssnD)ikD9?aglDB`Gc`?Z(t2zl}QMkB)NY zb{kG^bBrTzF4xJK{KGWjF9#2;+i+^R3j>_=Rz=8PT+IXaA9jrRQvk|yI7E|0F^)2p zL}*LwH{v5%ujuDe|7E6v1asR;j z87VgYc}ewnyWNOvSY#3$PI~f`hgp2h?G?+A->85!B15oULjj*HOjsA*#mOm>ad_DM z*5$AQ8J7BP)T%!jh^vB|D-a33g2|1|GH%c-vE@cYrB-eDm^`0l#_1v$V3*|a*t;x- zzrj(E=?Fp5Aj>~blkxp^XSG(f9dZucpV=<_?(bP2R#T~E^UmLN{`xNeED8U8=cQ>D zzim(8Alh}`a;xYM7U+CTaI>IE`3~1&xY+?;=wyOd7Qc5mzzS@fWM4pEtQ{O&;%Zgn zlY%g1%w7@>i>t1p+Bb&8NCKTx$3-uYsrBtyOY;CXr@x0s=xht;%=7rX5=`8ky}to8TLe zSCwAjMmCzril@B%J4UVm2tQZg6Z=V_mRwixhKq^-Gutox)N~q&Af${Z7v!3}{xkmz z{HiL)&=N#t$jUUZJ;vK}XH`{Sjm=gLHrk+DYQzWLJ&VY{lLwiLSJ4_68OkdfQj1hR zIuAs{B!18ai08=w@H~qmRv{%j&!eBHBHixJNy2YQ_7v`@CB~^Z!eaN+CP~;*g}Hk&OD{y6*{j4^kLAVUU`pk?f=%#<*OID-A;;Yg}uZBix!}g|$oXg|ce(0;`8$ z)DMchK`SJa-mMTp*2Z8>!NBgm8<+BDbqiM=3uD}BM{>NFeKzeebl|eR&K>60v?aPg z+MZVCZ09MkAS^03{k9imyStm{(3}TRKOMN$FAEyS6mmh6(9jrS^*xtg&9)b!c<^Jq zUwV+>*GMM>wcbnOm!8|xT4KEel(6o)Uj9Txxqla)2l+dBBNMaSYG_A2$aK(Xkioct z8~6?`QPleH?kOE*$HATgR)4m_+&MX3@{TJahF!wD-{(^DX&8Lg3(Tc{KiIUH9{X-3 zWQHm6PU?p3w}0N;(k4}COokK+Z+!^K`)+>`b6==|m|+M@k%iHs{FW#I@P;@#Mkd4^ zJuB6<;CSy%A2$e@v*3Z4Gt*iV$4T}tE8bc9r-g<0?$agR*{GQrV?Ta`4tAdqEu`!<55UBFa$Cah5q(JD|fgXqMpU3P2UB&Mtzm7akP9gd9^XG}g zI%Tm{Fpbp&S{R*6i?PM7cLxQ#dK2Fq&67rxCL~aNG|p0aoVS6FMR<~yka7e>#WL~v z1pa>g&gZeeD}&xWXFFXRdwM@(8T^GF#Uy>Cq9S>KtIGOi5X%rJ7sxf+8Zje@{@29U zg%Mji<^5a553`PXF=}{w?h=CtH@=0N6h_Cz`X!EIv!jCqNQs zv5dn!0tAh9BgbC@bSxjv|{f4tsc8dv@w#9emFKR zE+V1x@5S;l7}p0=*H@*TottDp--GYgS&_(qZlU6T(6=L; zeQ7_m8}5JY6`R160dY>FY>#eSsV$xWwe&m)b^G!lBO_z4N|+tB8{I;igm%{?QE9hF zMa~<(U%(b|V(*_f5blWlB`s|D5m6tmH5&WJzC%rvhy#qlL&PpQi_T(r!I$9k~Droc*wH!IVhnq}}UD0{d)Z*geyo-pLY?yF>jpdFP9*0rJbsOB9?x1me5iV)6hy(XKaj!;iG9>+8O4TO^k=B6K?H)nZRU)F~9f zsUGWIt%##02;8up;CQa2X()BiN>Gj&p)3{t>@VaoF$MxI67}vmVIp7v!Y34k$VO^A zrwr4MWa9aIW2+=Aqt*}`Y=)UCvyEoAOm{dl@NfBwam-X{yrHu_893ALqkFWUQ_T|b=^zr{q=p%R_L>B(NJ-KV!0N%CJCniVM(F9cp5 zum!X-qfWTR*i1-Gr?SSmwMZ%pdXTx+7HrkzJVKwU9S*=AfVQ3?Jp_g!m6Dbs=}s-f z5?@qC!_3UwavUM;a7Sc!1KFu7K9u)ZUFZNU%<4B*NBlNoZD_Y}G^k~c(R<|jNiG)r z_x0?xY3lzYe24o(!vdZ;$^?jp20{cs{0-8t_ioYj52XW2h0abr_0iI^^$lMlcYxfbnD-8kq^ocQ{i%VZ zejR#U(swX&z7Na97i*lyD*&Pb()~xIgl{*iS7Y|>C2PIJt6O2ukiU_$5U>u1=Xw71 zmN9PwF6~9vQQESP%-M?s=Csr+WmSE>Rh&k-@A*3Fd2kChjb#w;?;fX%&kU-L%r3%psS=ALk^Ek z>wLfKPhik^e7sfkUwTWO1@m)e-&u(b&(| zA5<3Z%#YvMTJ1`l4FDw#=23MM>X6)Y>8MOSO;^y9XG7rb+Bb0%zSA!r(zXg++mD;? zY}saS&7Ag!rcb>w@F~=DrPmG~g6~c}n|7+DgTemSZe3bFNky=$RA{hPpIm(kVeiX% zx-h!Q?Z49`!VKV3w;s=F>RgX2Tw=xKP8>V_c3OBI+3MDF1gz|U3%Hel9J1BD_#PvMZfGd-h!Q`(u}+` zG+!ooaJ}qd7pP!En9JbDQARbp+p|t(#bx&{=+mVTaG;awJfYVXbPIY6<`plxf&Qfa zTag&G-%=`@S71hl_^9b`Exk|9CtGqH^NA{om~*L-r9#MS22QcqN^@0^bJT5N7)@?R zhs?{vV{H%*l1G_``j(W(NcOUQ^v#2Bdk})5u<%=@ZD254e;_c(?w1XK&WKI79036h z9S!WbbxtLsK1@)xoluM#;T;7s2N1!_nPirve#NF)EAGuSSJ^Ukb5^jdd41icN++?vnxJtx*)Gibo5rOwi=C%b6D>Zm7wcGy#@QukGX%WHdkYoM+JTrWQqSKERgZ~vX4OD;_apZ|h2 zWM9j9)qdb;7K*r<>TSRgz)X)bqb_N+}mb9{dXW2m(~!`q(LW!L6nc}nkfqIhrc*|_VHgu~WsJ2)Z#gS@R56opUGTs0MGDW1 z8lwMjM}a_80mVLJ2_%#Sg)bTRb0C?KyiY?x=72Yggd7i&Q1tlY8?^xSSCOxj4DkAS zyoy@tfw(c|5xVfT2u00Uzh=KXR+Gd*xI{b-51lGveprO@S6}x~GdJH-DOo<*T9hna zJ6Q-0Pcwg}8|bfyxC+)WGQ_FTI*Fo?68KEVb2;!q7?CMh$`fzA1G4AQG%c@{nR0bZ z$lz@B;2cSjK4^=!1N8^E3IsVL^6KQHk|u|cuPht-UII>@w%ve7vjf~qGD*NeB=_Ae zZmeYKtk~2Z4VZT^PQC9XxqrufvXVKIW`0Ec;$9xf%&FBBbWX#;!O`=yJs5@g{W^v% z>OJt|R!WHP)3S^ko$uU=Jc8@{_jby##_{*n=D8#pMa=k_JJgKnVco5DJ0O+$mF_rI!gqN7o6;f6rcSw1hx zUg!T-1Y2`H;HqQ!P*_BShjBlt#U%KN9)VjPj~DoOI5|#df2^~ODa`MxsHo`qEah(^ zCpYjrXGITbLUCHh=_|V=(uuNOEIt~=C0jO>-pC+Y(~#+GJ3k{siSwGwgVJ;;v8_%1 ztT*tkD>(=22C|(u#n)3$n8#T9V(#&$e=Lhe8;lmHDp8aS_vSCKM39>q_RD4DXdPfZcQ&f$a zGz^tqVBJ+UUUZ2}P6adWE-LrE&}E~+_!Ry#l`A_hVP|@UO*%?x zM1cukJs97EZ2ZExo}?{ZBSUJ$a*P-xf`Y`FZ0sDYY+pYfW_B(CZXN*+Ub6pvQGE600<;iNmy-G4 zeSN(NQ&_pVISH_`dU|@Ycyh8hx>&NZ^Yion2ZMuy`HO@>get!Oz|b@{~Ch5)Bk~WaQ)wE`cfFHmx&WAI}6)?hVgeub_GLH=%KvobBp~TxZsO+XqUq>p_upDn zwQ_WGbhUDHB9qkMA*0i_b^tnhy3+p_y^@juz`@nc#KFuQASF!kg}`EM4HS@M=i}s% z=9ZA*KoGW35peR6~6QqUb zecI-9fcwESnx)eIRJ-H#c;`pjXy-k9Ca+>CQ0e8xIZUZSlzikGb>bQ zrMat9D!+@TV2wIqh0l=8E~D+bQ~BQ+;R=Y=WXC9VgpGqnM{@WK>hb z=5m-bywc|Brkw5Kv6aQ%Lm3jvg&IQk^9sfiqo?A*SAHjXf^Rni3Q&NP(*GL9@r)8j zb*6pY;hVxXtJ0Nz!@ntY)rCED)33_+`kI3j3R=~SD54_}3dL>LAFVn;NWv87Ln9|b zQa~g@A-RyZI_YHE6CC;j(*WkB&r%1uTt|^> zArvZ+Cf6Q%0M#c7^{I*7(FL+<_>l(^x}NRv^=RSYPR!+tHp#Znhts~G~SYQ4N z@-}K9Tc4vDO`(z9Dx%Lp7-^31^3!aAp}lu){QjuuV37^TU0-Nh&F>L>_^FfbKnpX#;g2 z0fkFlqaWfAT)X7uWSl*`7OlHwh`N@vI!1N*x;;(9y_RYvd{y7(w>`;xFpEridNvQX zhOO&7aaB<{$(ppA+(~#Lof#1Mz&zwK948o&UZn1ngMaW6*hqm1N$Dz*a5+@IiGuOg zTNtyNrk?ei^m-k*iPhs~c|0M?<;Is9*SINdhk>_SyxJ{X+(w=Yw70Plp(JlleQ$jo zxHF=g?SXd@@#&P+Skk)x#v~D0`%G7(CVT@wUr*PQV>36^RXLdI8{$dsV zB=JK~7D-Ui#6HzXpH=JdbC%Z}De|}xhkE@ZDN;o%px=kaq@%@qo;v}U4g%G6A~>-k zpHrZXHhTlNC5ndIFO|K|BdR148SuI;dz+1_&@(Y@_xSr-T2_!AUcvP`CUMh=Z85P^R${n4O`)-6ox;c36seG^ktS7cpY>4j&f9k!Cl<2Nio z(p=OBu2x#d?l*I@ozD1;i)20ULmIKB#+JK6`1B4oawU!%`Xzg@Dp=B)XcB>^;yN#q zoG{7!tX~y@U{z|kSKBP2kS*4Q4|ty-0C=LnT5g*^ts9_5-~PbCZV>3Z=0N>zhPT!TXx7*V$NEAQ1)Y2g*;JB zE1d`okd~HlbP$PhzC%e}29iyzit+e>#;yO}j3Q^cu{>QOlZdY?X!D{lOTxdYBWQzU zPh?oXQ!1ahGTUUgaAT$-{Lw%a-QTmV+p^zOgR7FuZu^=s{rUX5^YJpK;iRXny<+rn zeU>Zw9?pNqvfAvs(R5S*h%wdG<);hD&o}VgcK1}!&{()BnJf%krhHz{Nu0^%bzJU_ z4f^CIT76xdkK`_urrhf|n(o^zj^(ZU+?5*&?l@w_Q{$|zsWDsl*CIyh%BqtjP)r$# zYVuNa5=ls?)G_DeWOR1}YCbA(JpIO)!JI4PuBYqWK}GX)zLu{@Rc$L{Do-LHVu&JC zo@sDs2W@g8At~5uzt-pY1dYg2(084Jd?pgq^UL`nCz-P5eZaU~FeQqHTZa;l*=31! z20cDgt2ZQlpFerG*L!Sn^ZK3r_%`7O+0zR9+|QP2Ndte?)MzX$hQfM~ih6{V@h1fZ z{+nhsuGipdzHf|NsW(E@zS!;neSX}&e$4`<$VY1x85!E&47@M)9}_Z4WZAX2c!=IW z^&f;l`VZgFUf7S1mDc5q%et9Sk^tkiyIH^MOFr<}v zT{jx`KYxDbwZHf25h?7OU4RW7>ieFXyRp7ruh;#zlbYAabt@i3tFq5ObM0weaylAt z+d6CD|DIA)*VQP9;V85SmiPbzS@=4?;_!Lc7WglAr#k1~BVm1^>*b~xotx8V09n>Z z1kCE+75zYsQ^sI~w!@yb8e3b@r}X-&n5NSf?Okd{*+r^-KoY;x;2o=w_IF+zsiml+ zLW9oRC3B66>F&3fn%vxkP0uUR$krJ&z`cBQ@(-Rgd%d>0pB?VYwRe;9jBg}68i}Ic z^fkZff3pUGD(I?zcJLD<{_#sxLXsFdLgeA*&F&1Sh9wB_baP9~(2A@5IaExEqk+vm zOiE3)?+gJeu#fZz-dHY}ek6PdUoxt^+2e{^RD{ntgY4;Ux6^uZoGiJ-%?&M0T~+_f zeBmEZoeEt|M<*jL=11Q^B$?l#e#3710c)s^?@|29;Z$y6Ca#b93!;|RlJOf|PKuFg z!|77ym-wR(#x6a^UI$Sj#l;b!urmcb!hH>5Si^U$oxqo-qjKeZbR-zgHwwC8P_&FD zGAtz@NqiQ`l9x~fTu*ncK2~aHT`sz?wAxe+UqUK1ydp3)4{cuEp~muVVAr_#rHziJ zUq6G&`X7JuwtmZgQQyw!-JAJ}ey+#(cSa*t=Bm|3|L=Sl)>l?#cpOA02nV=%-2Us5 zPdkBY2btj3%Go@e1krO;J=UP|Lcgo;u#Z>ra^q{RhsgH%HM0c2?Q^IZk5%x%w?JsD^pS$wXu;6 zxzA#^MUie~6?x(s{c~jGS8{&2Qf{IMo$tuyw~LS+fNEM?#-=+MKuahz!Jl z)+cO|>&O0sqf;nUabepDj|s3J&PH1HO9jNfKwW{SZbW{{lyp*tYv?!gbS>)=oyS2} z%n_v={tt*vY7=z)LV zzC!IM7K?_c>8WW>pN}@5%xGyN`aB@LRHDH!xKN@#NGD?D;^7Gs)RT=Tp_E@=o+dmU zUA)@rX*+pNhQ>8QSafuKDQYDe}>R)Mual4dp#iV*pjP;Bb> z88u-gRzY}MuaLShk=07ZZ^~!Pv~_-&(_^zYyg5G z={$W7IC)j{O;#PC*SI&~*osPBQ&Vae0ULb`@3@-vV`pn?X>Pl`x_ae|k&X!hpDxBZ zglu-KX~2np+01CYs4^uhYRC@xQZNPTgeJa#YD;PwkaP14A%3Q)M5K*_$#GG~g!D*z zt{4MUu?Rw%UJ#n>m?fcZJS`qe(dFq!AYq+1x#adD2FJqAVkGX0z{9CPA`eR)Nh+sm zqz2Q&u~S!110_3Ivei?eF-+x+Ee3`UC9h~Ron4-TJ)BqDI_h|>VP6-F>)?WKWdA;4 zys^?5oh>#=cIqj>8=#jXNRJ9*v^mx8dz$eN{1DE2TN3P@Y|@uK+w)>FJ^VWYG9aSG z!wIjgV)*luDq8G9ByrXc))39B$f#3_E{&8UjgFlJJVj`Ej*2lFJT;78b@ZI$tW4HW zg-lZw=FSm-k+QnA1`#&EzG+OYO_rxwgs%%Xwhxu%m_m`1>xa7=h&1f=9?&50a|E8p zav@n#Xtq6@E=!9NJ!!17a9Xh=;EgyV?|?o9K<(m1ZG6ro{WnK7-t`(U#tgreHZ_b2 z-^^H;+RLra&M-_yCWozu7 z!rqsIZ4pV zE?=IJ%zUGcHWyAE!r?}@PH9(9OBH*Xk(|cZ5F+H34xHs;5cLQrtS16TJY9-hDqg4f#W$2 zz^&I^l7p8}FWK3hlo}3HUE;-{?se0Tjb^0tU91!~PH@-}>3Noxe_fuP%6?hd`Gw>0 zK|}CFq7H9Rslt`&Oz3B6sqFJ~v0Q}?i-e}i3o2=p*o{&`^M-nQ>H%vMQ!A-z^yo$6 z!Oa15beixC<#Z_HKl!%wXV_j8d|rO5Ozk)WcX5`$n;h!XB3gKvqO|Cy*VDY!C; z@=*oDBKa_y5X9)JfDL0D79Qw~B8Ydp%SmfvjYhgK9o@YQ+`1iZjLk!_8S>%7`HW$u zpiTdCDcW(u+n4m-mA7K2ryj(jtHXk$z)s`Vm&ssGL`K(*ktzc!E@91&#YS9663j1J ze477co$ZvM`8;#=KvtB`0?brxH|M3XJZTqgnn%iplX5rc+UR$fi{no;Y%sk``T`-Gv-&vhes{;2z$vn^n*ot!z(_IFdMf+DTZM!O9BZv7VPBB8dnsH58+U z5S}SVy8D$QYE~?4~PVGa7E;9gPp*fWs zHSDLAF0IjMGQK59p4NB!cY%mI_JDe)_OjVt={rG9Q(CsLCB)CNndEXg7Nszty7Rr? zAEpDbM;5@o;;K9(Z>Wn;%=Ak(SvbrySy^Q#W0Qp(&-7icbO9R=R7n!UfN6JsFuw`A zTf||lNHU+0JYn7Tfb$?j69dg(u`$7*6`uRe00$rVQFn9Rm0Ji~6^v>gtE@ayb=q_M z8yhyH5^}8!Y0d^=H>67wID3wCs-XI_OEG1u94f%XLLc1YN7n&)wCWb$aLliY;R}%aLeb_R+!}xOs(HY_?eLpnU!R z$sQyHuS6ew<<5qmpPNiWwJm{BmPU5i6}pF52^CHh`K}3=B<*1v2|7ojsa{3&^C`Le zNIsAdIcfQ9Cd?^$&ebtpN>$aB^?6&tE_qCvm~mR8;lP>L%n|r1yEXLw=Md;{o@d(z zx{%$67X+j;Z?wN&%YF;LRPeqTLl?|`%}*MZ>{t~`NYxDoSn&rsf+_t!neL~eqo}c@ zT_1b*!ZZ@*xD}dyt*KaOC*+-T@@-KI4i?c#v<2{>6fP{Q@Jds|QD=!o;J5^h8 zO}9s>n8cAvyU@V1G0^AGS7F&>ZIeT)VDqI9B`=%d3o8zE5hxEOBgA66xD zi%XVK>%8ct;-CPN$$%Xpbjy<^BPlrgwP`S8D#1?hTlMllP)K2kO3NdF5wBzS2s)-Y zEzye-BaGW0UogFxo+$Pgtq}8f4)6lMY5Q9MU{_c`Kwn{nqjdv}XJ{ z%L3W$Sy?+TqqSLf~`JJ6)qaqZlFjKl$~ncikTpkENv~n&{XKZ z(Le0@K{P3Y1}e+&v3d5iZXDaC;IuqPIhgqy<2v%IEzBG^W4(yz92Dfg94PIq(DoHQ zh*dJTj+JF96F4@4kYc=%eVCN?(?U|)sIRv)4YW!XSPZy)XToz(A{dd+Y38@yN6r`x z15P?pZkuKUU(RHR3-|8?rDD!Eeq<*?_C$bQ!{2;t36(g-Nr_VM1eEi;h6NBgY1L_A zY7C2c>AjsUEy-G~eon_@rNA2dVcm=!@4J@Ir!J#RZOxCz*{7uh0{e3_ zgH&t`+@2pA`IfP8rcqQPhpr{DJy4$ScO${EEzLQgZY3U{b%fp z9t+5V8*m#Y$2NNicD7A+b#?ggaN+yJ%AKTl zyZ_>e9;XbuseQJHwJN^zMbpS?O=xmm`$Lp_ia646g&MRIw0^8CpDdWMdJ6np(|G*_ zazC1;)+ClMP3ZC-IZuXzK)h{OjNO|tl|!rrr)$p=1>bh0%cDWwuFPdSv^R$z^+kun zi}AmW@81m7u@tJBEoSjKJ^aYf$l|loU$AQf#KlEaD35~1cT=F4*_cMvbS2!O5N{RQ z+y-r`X%wnQEwi`nbZ+Qe|2d6iyO4V$dsd+9kGarX20Hk{LVB)o_)E>D*HTi@ABohk zF;J7A2w67HlY)baJigzMMhHk$9ZGq8rWF zt21ywYW?M_5XDg**=jB%mjRbyRtp%6bE*i8J;TudBg~PeonRIhWirzCE1yWqIfOS> z_;ZXI7N$4B8H(7945kQzbPun_2Ofjmi$#gdm$o(`zX1<@k)z$@8-e)&H&?vxoGr_C zYa~Zfi6qQxV4h=yPxosnr*}SHZyh-|;rIZjS>xm2d`$7a`ec6GeO-b@pXjUzzn^ON zRL^0HbR5i@+$ENp)m0Mb)_2{p@v_p(vaVOfYx+>0QlKn>?qele@z~AdUEa<|d z3n0G1(#JckNoTs5E8wl7&)|I%n+%i65_WKDq?W7kKYw_>coOhKJTlG>bioNnx6~A! zq53Ms@FY6JzV0QY7ziR%z72Bd#_;mC(ZxtSl2o?6hLFLPl~FsPh}+QO6HkFB7+Z0E zeUzs`TUR%)f`WP^{?}LeFnF!yHZ)|USLpq5*FHUM0f%=*#Tfkf{PxN^6I0`hoB>Jj zDZrP7yAWXnsb$PRV8n!wqTa{PmBj5vg3-R~u6dD1Jq8!~zO9Z|Hdk;kchJhK-42qL zo|>d@4v&u{wDC#J2iBlgJtTWeYkUz*=%M}fs>W>o`AhR3doYc>uSw|{e)0<+KH$a4 zgrNC3>Ey12yPLV?QYjZ5>4Uc3Pt3c}uZ*Lw?Lo{Pu0DtTzjMh*f`qrfbqhupW2?1= z#29L*>qZ_w$X&IM@xtk|$Ul-k;OmJ8peA^bgumoKfWQBsO8DmLxJ zRYQ3KYG6Gh2P&A}#)(OOsZ!*7<;*n9yyhCHB{K907$CLarhe%`#0&?UU>OSCTf6P4 z)I7BLjfxC=gho(ZOW8z8IAX`m$pNfSHGW!lBdAy#OA-r8&SLXP(P)(5a?*A;beRpd0;;-;*mBVd64q zP|ghTB34PX5gM~Lx83xWDNIQ4U=z`@_xJXGS!|y9I3?b>frV7(80S!y2y(WNjBY=S zB9KGkH{nFq=3R&T=hXz?=0~++2=Pq=;k>N97U6zF^!*ZdgU2z2YEFp1AW9|AZ#isS z12HB>JXOHYN!!FESiI4ypVd)Af=db`Lm$dkuNpC4*SO6x$X2L5a5TaG?vS;% zn#9_}2#k_D&1m-lS_zP%ID$4IYF|fN&bwLI?0o^Ve*RY&reXos@|N5H`yyZX2O(-O z5T$@oB5@dGSok?%>j($cWci?8!OyRssR>P=kwuDAnA!uqOVb(Lf|F~`mUf10zUsGj zEvfv=x@l@>yhRQybXgU~MwJcdN1P=)v)lu-Dns`7`}jZa14*@L^*y%|eDiC*Mt+D1AZKMkz)E7i>UUm* zrwBJU(E9TIUK}`NhovzI?FOI8NV|R*xj`Asp{l&KB*cjZl%EDps*1?tRd+X8cz&3i zVvN~nV&6lv(HNph3R};00?d>#z+p2yihb&%)~M!NG9MB1Jh#@6oD|*Y%DJxcQ3y5@ zGE=AOUJ_4&&G!h*n~Z7bi&?o+nTD&OWHu%h$st1xLyd@>KqGefMQ0tj8KI9(6(*hFe7A`dU}gUC2>ZBYCEMh4l|p`a;bBiI!$nL;G`P;B#Pg zr3suuP6pM>YH~zHp`Y8zqkSfGr_G*u=AbSXxZm&IVrc%wr`_cxzA^TsZ`YSw!a$X| za5G$rO`Jblq3pOSV>6C_9DS04hx3PTrhTJt=bOHZKA8)btxq9A(PG?4F(EKjb$a-D z%DQqChexLXBz4|c3=dOXQ0;~vgy62SUkQx_<5)Yg}l zZoZ)n&0K6eL>g^r@c6BgetyiZud_RiJ}#Fy6_bUXUpZ`q%Vol+TL8q-V9NObax_%D z2EW<9-*~-+0wpe%~vaw0jMs1$`<&TS_>ub>aZwT4Tc*gUd6WUoIJoF zO|&wtd0t8YyMw`*ZAT}?kWlzdUYXISy7Cmgx&xAe8y*60nhPDjiUkU7JKhfu3RBE1 z3IMH?O{_6Nm()}UctWwBg&%mo;gEMDGCX2#tRRw zlHD&@WcfKg0h-3>#7N{GbS!g)uRg*9-Gdmq8JQxtyF;JP8~k@OuSX>cHPvV8TmZ)a zHC^r3o1zl}tjtt~s`R0nz=ud*;`7&q<9iZ{XiWVu6RSeeKXcn3HHBO6|IQ{6ZetUP z1Rh_F-gCHeR6;$@P<>c?ZmT=)aGZn$v2yUzbXOLkq_LR;Z%z^(Tc@5O6*(rIacBD2 z`hJ9^0ZNM(&?-&k($7PACa+!RxInwj&5{%nkT*zwkBNw;@~uT;O;)Ruxe-}=5Ttm9 z*!-ivwwf<~hKZw={r4J@8{Q3WTF>w6w+A0WfhKBd1oCiGLZf#vO!>-mv@*kgELNY2 zP7!7XS55+06?I=p;wYE%8?2kf=A(+*6|Fcar;d>9VRFjoDjAhE*>Ao04|CGcpyMIQWANg1Y{LB18lQz#~7; zXU1*8C7g>BJ}DiRe#k}nAQX5Q5wS50+G?me9ZlHibiM$g_AYPEV=*x3Io*8K%4UuQ zFP`ThMG9wiic`6HHl1K24sp!DuRNL5x_%RO`J;({L-0$w)$eWH3 z4f2V*JT6y%IZR*gQPy<^z)$;fv0F$AK6*f$rnfb;ZJKgEY3FIKWBoGB_57M@>?aSElw?su58`pfv|yrlVJ9L))QbcZ zwxgN9stG!9r)mldCS>qxY0KT8%4TMg5{g*{+R8}cvcCb6#qCYh5m98*gd^}wl_~}X z`CIwjVP_hKt@L0CDu{aVG6WCj#CdB~-&=09|K52a?0CO=UN#k{SzY0E~g&6zyI; zRaHLUnU@xKvl%eJt_Y=6h6GLtqFSCR@fg=U@vJWYK+ivYGX11dmKt@%#noV-id>4~ zL`V3za|cFI8ZzScOAo1m&#+dFVcBy9T=XSPQo^iD9@xqjG zR()89m@N2Px#S+g;aF(!NKOFO~5o7}HRY2$!0PX>| zf1HxayFiA|F-wVO&A8us}-HCyB@Ec$it^?%&POps)=PCT+{gu<2i!A#*&YKOa>jh$7g zNnMV&IL01H)zO&kdXd-v;CpaID+-sEzSa=6*e|7D5Fe#($lb)%-cQgme&pN@DKc*J zU#6c!J7BOMhg)i7y4Wlo{mW7Hm72e0Xa~n%pq$_9s4hOq2~nWml794Y%+Z0WP2IuYCBp!|*nq;-XloFtgR5ky@@1UakigV{i+X z__-TJ&(A{~l?wGeBMsC65dEFkzf?x^q%aZVVaF&c<-rpvHRC>~JXlL}o1Bh99hl6Z z&=*a;ZzKo@gu(BM1jg}l2XaqNN_d2ytikt|X?On*iH zTzzdpqsr>s2+kLD&^VB`ejX5Wzyv zJ?5Z@iPP$kkm7|OaD1}$@$FJLlJUQ0hc6_-ofo>vK!cl_Z^78ud`l$eIiYw$M9Bj4 z*Z`lC>@pCIGJ&Z1h@fL{tJ&GeN%Ksu-?H1?0jTlv^08cu&9NWf5bi;#k$Qb35Khok zhk(Z9#Z`qtu8_}J-ug?M`0b@`4jPM$OtzI~7)86vD>JfduGCJSsdD!0h zZ0-1IeX-tR^s?SoKAi_WiK!x$qCQ^%3%hCCK}(75tIn)HK1OfCmkCD?KpErZiJFtM z%_7##FczRu683#*!Zk8_bWOs(J2c|!4A8fhUTWAI3LQWjvnXVhLy@>i+R&3LsScVc zu!wW(k~tMNIQ^B!TcaBx(fPxAm0caf%7ILAuy;=k2a6^ty@?5qj_A^`&-w0R=ilYa z9q#tsLf6dKM8LWgFv{EKw~_O-1D(P)2JbnPE@izNLygv9;}%a&RkOX;Zv1B=S`l1y z-e4wXZc<{!wZhP#8A#_KAN3W!p7~Sk&FnYjOxE|31bp0g8QJS}`Ana>Pa5hQ8W;%s zFV{MQMivtaG_`~UFfN^dVt>h&Hrc!To9t#z1ZYmtr&=^{i_g_1(yVZk1)w^+Sda0= zBxFk&FA?E*9x!ZplEjjTuqKlTq9HL19hq>pTD`vp@aDmht%@}bdMJag>nFiv>D+Y?Zo`Q!sa;0r<>*9BoOl zJI|c+8$GWRn^JV#&%u)VKL@uSW^@Jqn`Er-F@(OXta5kiZb~IRKeu`T&OgfXr#eeeYH#$>W$>e z=QIo;*G&4;&6cz&UaPKFTHbjo@%vVhZPt?`xi$d$c*K!6T?OcaOOd8kcq)jj+2&^&*$c* z{E!m)HQ>7W6aL@`al5}Q!0Bmf?Fd;Z86SnW6YW53{M#B1UWelDRMuG$dL+KLW#%+^Q7E9z()cKB0g<9J5kpVeztVdA`Zzl|?G_1d32E>th)a7tZwRdEng_g+wTdTc^~wXB&Ica0)RT zhpF4o?R`~@O>}-sfXg7%MU|?Rp}yT^ZfS)Df%23QY)-N*pDPAFtI|nW_lUkaTLCQ$;A^Y(Kgnk2C>9_;|&hbTU3zT>Lhu zCi&f4H4I8Zj|!b-VQJj5aH9?8>lD%TpsPwt8y9oO&9_vlsD%yFh-Bt{j06ySo?H3d zK-G!&dLMlr1E^YAV^rBNeI8Z}Q$`ZgHZCcB76lRMgML^Pcdb35_A_=8P-}}iI5emq_HlKa`S>N0A)*svx zEav3q{~i*{BI3JV%gJF7w}wJ%Ny`-MQ?i26BcxhRn_Q~TFUIDQQyvvFgA_)ECEcu3E`bgo}Ozh}thTgDIn_EXMPvsI{Gu8z~ z9369|9pb~e;pFFWAk$aq`*8YutLpO5JCS(ZYN!ZZ-DGRK37EF9M>9}GL5{-9j2apx;(qDr{-(wyR7x=Z4mWR zdC7D}sL@QKWTq{=oqA@h)imjHBC^HCw4B2!0Z0y$j(H6~qdMyhT8~d-S$q7ZyBzEN z-dmWzx4IH>`bizcxgHrf4M-GMbdMM9Lzawk%$WW5s#XrUT$IQ)|M@s2PEn+RT|GIk zIWK1n{(+V_+-kq!e8A1S`7!Mnc+;w4x6x_6{eoEFYkJZZUK2#69F}3%Wsq%uX}7H; z`0y7j%=Xa#7J@g*AWGA_p<1dzXT<(Wu}ZnQ`Di+l)IdYT>nfkSW=GKHZ`2F+vKb$Y zS55pxiA|@*cLp8P_>lwB#VYFP#I+(@hN~Op0+(-QAoAyaVY0obcOArFY2&&O)&!fyAE;mm-j?S()0_lb`{5KjeoposKb z;OFGk{@fU7mgReg>F^w7q^(TCdi#fPh>W&@F9<)W4)Q)AiA9K(bf6oy^o&f`vXIwu zI^*5{A4>rZ!N)6FBkFleDn?$~bLAb`!mCSV(Oznsf4Qkn3tbG+Xo2fxM^3q&e%Iqv zk`z-8kd9mXHT@i0Yp$-{9Thqs7&XNr)sRn_#EZTw3W;OJ{AhnQXa9scUJN^F5p@wD;f##%G-dsU*_h znp7LA8B!Ee<0wfIg2&C%bK_q%tvcn`xl}LRe*;W64~Y;AslW63ZW>95cOF*1WY(RS zsuPszO&6p=L#|JqoX}I-wMS)6q1an2*B5#YgfQo}DrNa4tQ@_+_5>Q^StZY zEmwOO$87CVC6>g$D_{huj;WgCFJtsUjpVjqqOyQoVFh6BrJS)88afzEs^bnEmH$}a z^(B$B!(^r+lFEhvQb2r@!I-}i@G#Z{OyUV>Mmi0;>z=}5n(N3$7q}$5ngrd4suE#_ zE|0^<imO=TIfY=X+d(7;j2AJGW>35>- zsq#Z&6tiBrfc|lFFD(@fnr@CLe-qdcFJcJIr@P+fb8_m+6*NP@Ux>8Jf!pWHR0_T+nv&l-Otqxm<%6av4z!>BA`T1^rRzAJceK+$~Jn!G- zDSR^~i42PxM=TofwS+D$C1PW$0arxEpv%!tm9Qistts~le(uJQmiTz6+QD;bfIA$U zDyb{~0Ob0|HBeMGI(5hBezWvoi$4Fx}kgiCqK$=h1GdTCVj3zxx zg|UVMvZ6&Vq0VjS&3$k^k2?*1SZc?yBQZ|i7UXYO&WK#KvijkRtaW*64-NPm^?<(L zv2;Jbdu|7q9oSyj|NN@?(Oz_tJ6{UC6pHMTjtGO*&bd+Msa63L`fSTnr!$t7Xu+G* zI1D;`RY2kd{KQT{YsLY$w|BF%S%Pg2_d1~l$H700;%HOH>AW!$r%3<AH#=#lSb|DgxEu}zqm*PK`ohGot-h03QiMU3FnVo zfc6Lhck>@8#1)BDbRfAcS3Q^!bR7|S^%A@oi*XX{oZk-)PRR+B-@eg^T#_ZPjl#9b zs&ajEnhTek`x{oEP7L6m73G8WE58a1iKl}15t*f-P~imwXf zUe9-Og2^%wpINgM-er;c#v+orp$VxKu?+j|xX|`tKXLHyte*i$1n=4_L|x zA6IlC0jtXy=KRKvj~P#)*(d7uuGDt#|8Pji7!0mIzu@sL(wfI>04!fQD@$5hoMA-% zmnrhHd||S{kKvRVM&#nCgKV*0l{2GD+n}I)I=23G`e3UJ4+gGbE$hX>lma2 ze&Oe1@bLT+NA^M-55r6cf&!%JCX`!O_`oj^p5;jT zPod^m1d}Y+>pl%GD4dNdas6Z}06`WF0)=RtNK}g(m`Gxd?rGEAOlb_DFf3$lD5KgzxmEhs8gr-)7phgU-Dl!Zmdm}i^Nj$zKst=zgvB}D14 zjqiUCE003|4D1CNrvEOjX0&F?Hs^~g0-pQKvC*hfY&lX|!>cdq-(!j) z4Vy^*L2GtGm6sMe%V+bJe_y>BrY`e8Vs3xt*cA*W3plbWr6i{b3FFz_)~6DR1Unn!%^=d&+LD!_!!J8$Es=bQn` z4>#{4ihZ8T!C5Pwni|}A7bBbZ6Y1#)GWLXqXJ)w~ql5o0748t$POLeHDGVP32&Gf` ziI0$uRnQA6L_B!;1t%5kO-Yu6Qn((7My@YPk2Xw6bCf%+L&#zP#JQ7)^MqYPHP~8< zrTH1_;W(u@RmnGUf2qiL#76d8S8k#N&~l|l0qbP=`0E2$!2UxuH6bE)Pqu{eV;h=T z4v`i55?=RmBy+sww2W2Mi0{s}bWCL^#C!;s`^qM>O{UL(`d9mv^28gpu_A`)$0LSA zcEF!*#H(7vYW8v&VkqI&2u=aLJns0E{eLQSRW1HZN2_rRd-PQ?-PWM%1po z%fGtsRM{1i-#Z4nA#hTWtvy`SxrWbA^S9h)2vCPNVIRbQ28ltF=TF!Mt?JAiJwfvo z;O-?!jacE&T-$kS%Sy!DY3>sl>UDJtUy!0G8&zvz)p8!S3@}1Lx+PY zsApqpCuoM_g!cCKaCcW}S-IcuhYi(q{8%{BJ$CH)WIXQidA4ueT2@{W3Khg6ovM^1 zQY=Z}*?!o;g3kaYh(7YrKdNi$F1ln<>eD-0j>pBA8+`MUnwc?eFF1hT&g_pFqwLJKbziivS_1}*@ z>+$*8S`OcH_jhjl((MZ`y)sm2mo5n^*}lDdc;1ut^un^?g}J=&(Zl;UZCGDdKVi(c z356x;)fcKN+ataXk;Kq32uJ0J5#3##yLWB}l8A)6-+tru?(WVbjeDzW$8X-WO-v+% z0c?y(k{At#!7T9k{XCEJ9uo05s7au10WbFl1HbVa_2U?x z+P{AnY`*sPw%xmSoHyr!^=mf*NgUX_d(8OpW##3GMEvbHU)`|!1FzRVe)5!g^B2Ol zeE#XBC1s@z2M$zK*G!l=ZtIpU!$#Cr*VL?9@v@He`Z1$kd-c`v`5THB5vKH7M&Ve(ni=Uu#T$JUMT z)t1d0$}6j?YwI3;;2)1a_~6+yW>wZy|MR}Ty!_Hrz^qrj_VQ1Ec-N{|Ux~+}v6#5} z-FF)g?gwG=_8YG~_<+Hbd;+Nt;8#;!L!8od`M->*7yubR5Lyh|`y7aOsXV2WcetjYt-L-p1 zZEY2taNdO%{rG==bIBD~&z?K4Zp`QnYu1{6)dlkx{^8#HuekPxKwh3|M8gcuO;4F| z&ey*6gHL_tOE=zp^II!l1$}ARvrp#v1K;@855Dt*|6cNmPi6IQ<5_ zH*drny`Uif>}h8M6~W){zV${;b?t;HvsS+R5@>f{_{N^gqr(DG%^?A*G=%lnQV zJ$~kGiC30C|Kq#vEGjORzPu=!~s+yXit@cqlYHLStE?oEKYs*K~ z)z^+5t;o`}>C>Kl>ha2|%97IZ?yipe|MpjgXMb|fZ`xX0UV82cMV5PgO4;-OOs7y! zX|+**a=h_yabc0*@$_DpZX&<=>gz|39RA*&UoI#tI(DS7rlt-g)mhVK%sO}8(uW@C z>}p^2>Z|ws=C@$qx3)GV;!&{m!Q=pY`@9A71HK-w8l$K(4s9YFwB|X_>72yj!>Sft zu^2dnIXtkRy2IT||NYPA=Hs*Ho}U-W8`@_dp`)mzn4t8-M-E+a%?&tut*KL|&-(Kp ze*NjsezCB)WdGh>$BrGj=K7n)j-Tic`Z~M1D$0lDbbqAl`NhQz4F_Y;C-PUzL>6F7*5bCXVYU3mJdk}$(`rVBFl(BVxrm1bCqJmnfPNcgg{ zinFHyH=7UtWO;tt^s{|F|MXe2LCGnvsGKli;uDWPuyNh$qM~9#C&Jy`a9USqXDk*q z^G)DgBjIi_nS|F4cXuH}s|OJgkH@+?I}(Y+4L9F%&yRli(;t3k{Deu{wr);}Vrq8A zfqi>6Z&-WNt+$;sXP)^<%kiccpIX}7biA;rc>blA-*xBh-~HiF@{Dd$mtT3!L;t+* z)t8rFa`_ee_Ux1-`Pv&k(W^LtmPVM}=SfXh3?R3DORcFmr-)|yrLSUP2WQI~?@4dW zWn{ttkI>)}O%pR?A~yk;ie6(Eg(ssTEBKp-RMyZuefIFw3+asn2d!x z+axi`3M`IX>LitjxFiWpSB;pZ^`6ynjcw-lu!T6zQ&v%Jb8|^dG&dc>X;k)9N%TNv zTG4FQ)PbJdIH#Nf((pD0K@Be`wK**k?&$7nWjUTCsH~oNt2_>)^B4lHfu^X;_07Bj z5t=WUf9~A*wY6hLj~xf5Ff3X9xJf_$`TtIwG7a{6enG)apSrEIVmQ2Zef_u#=3gSo z60n}nedQZP#YKXE`-_J{1&eRGWmsiZFc`Y==Fe~jl0M6_7hZJPm@$()9`A$+XMN_2 zU)j82jnD5ZtEd2?N`+>WrTF4c++1Bdy6=;-XUu|Kq{?xYtvK((1&eR`v`Ax)EAa^`qE0iw_j08=Nh#+ZiROp(B%fMJauKizJhb$1-xzon?C#17odz{AC? zgw9&N2aE>i+HtAnX}zE8NXI#Dw%yWTEyCTMc?D&rs4qbV57lO!o^X-z;0 zGuw~OD7e9eatH~6he}QUCk=o;O{X#HX}?>+Wafp6ib^Zt)x=~Z+|>arEWfyfEcGNo#B8(@#7I z`kxd}-un65Cr+9If3dYV#mx-I02X@^c_azgO(JQcD)};Z@tjh`68x+z~qJcTW z{*H7tgSI1!G1y!bq9dKnKyA33EfImKYSjslSN$G@bh#~>S5yJp*NWM}{IoS4)sa+K zQbDuas@GmQdgMqT=-<3)YU{=(6WtwcM}vWaKz<=mc~a``>TCh32nGv71tly? z%PNY7TO~P3lN1nVJlvrw3hxboWWp&Lc-~jP>B-R}js5 zx0%C0r991Wdw1=4?4gHPj-7DUv^A^Wdt>EmJ$TxZTtBA%(_i>%?WkH^PNtO7qa2=# z%=X3BXIRj`%SV`GZ$lEs)Ag+97%8ykxBaS?Q$9DovctyIT;^*UNIY6rk}!t=Um5*7 zQQGLRlpuxFbaA5^KvfAnjnk1dsgfF~)rKfnSyteA4_<=ap#?*QrVWOWoJz$oSTH0! zL0J%xOz<5zVJb09F_h+3 zGD_SskciC4i{Uj%`?j}FEAB1?){LA;vO(Tz4019F<_?8Hr-dQp)S<)Jb->_Nh5}oN-*!OgP)D7#|4?&{Xr6&TK(zI4jj`0B0#^e_CqQ zau150!BbMXi5WCX#h%KsF;~Cyha|28Nw`7#%etrr+MgnLc$e4qVG>}SFKI$EBYQ~) zTV4=oqu&p*aP3_X()nW5?m*{}5xMUq*os7=`K zEZo+39T+ZYG>bD#7%~;=<^)L$tOZ!#RXITMP$!r%{txM<-Q3=C?B5SO*xl8I{ka4x zEGWEU@#4C=F${h+9rl-jEr+shU>k*s!F_rxUHe(X1u(^$F3Snoi5T#N0xw9h8jVII zyg-mBdhB?|o_(>_HdsdN(c?YA5yK1XMhT%1m~Fa&vxh@B1de~>c0R_|0jfa?jFdIV zI31*&<5;49yD*^lgyd{u>9~}f_6B&Zm%vm_BYUATIG!}2rZWvl-oy49!>Jx8%%%{b zWn~bgu}M#8noWxr9J!+ zQ?=Ax%$z>pkW^1LBK&~@!54bxt(AZM?XOD8hI<2n5hF&3qS)Elde=9;@|8QjbKymo z_=82sXqdF>!eEQeZI7;%^&5;Y<$5*@Bd0gONzq&&*tvJ_ftO!7x^a`5NC-5o$%g-r zWkp>N6&F{{n_G3!0^aXel1Z|cm0-8MF*E~*+(fJqrZXxQ9+zJBKPXX?OEIKdalD>~G0;ML_XjI0}T)2*M$%g@gbg_4P6cckm-#~yj} zwdJ_%q;`}d#u3s_Khe5BnS|}h@duYJ-~Pf&(xF2YKJT!y60gT&GDSEm9ElulJhbWI z!%bVaP5Q)*`E_-gBq)P7uzCUcnt zLXlO5p|ipPqZAwKOeBXRkmfu^Gh5cLYd&`T7r*~YQE^E$99C7mxU>v@{_8tG`1#%6 z+p}v|-RLnC^Wm=OFgjfCdG^^oPd^h(ijzmxR92Mx{Js>91^h>^99GtN^!WZYYuAUn zCw}TvWoJz$lQLnuhdzD>8r(ae+-`|nw*eTqEkQUpGk17;B9Y)Z9_Rs#$v0kq4aC~B(UJ!&nfWQy=h5<|Z@TyhYAAY2aV`opF8pzKh6cx!b)@m?b5Q#()W~!*5pa_n_ zg7?;K-1^YJIiELBRU;+4jpo@sM&VHG<-_7}GFu$jAh%mOA`Od8RaL8{wAAKtFBWVV zRaB1f1_L{`ZU#oXe*Fgpg+*|FYfJMBPc2QxBO|J+5KcGHZQ-mfDcTXKH*?52dN%P9 zKGeABUk?;>{H#gi0s$W(N*Jt+B4eVo@nEBpR7n~P#iOU(w0r@ z{`Y@=Qd%+M$e}|c7FM$?yKc?u)JaK%`>&5cHC=~%qaL3`J*FX9+M^ih$`4u0{^rSqf z#Hfn$)|R$|n>QbR>+Pxq3uz3T*K5_`Bt^ZyJI9F3qTJ=Lt-p8xc0wAxa^86+nSrL zmZi9)_-DWR{kRE}ND8$@x`1BU%m@ZaV0p4^-9;KVFKm6Fj!G(2kb z`2YURA5-Q?pK;C{^Y2)s8>tc}Fh;+hj1{$Qbrr@LLUs@0dNkzNp}@Yc}3EX&i$yy>T)e%ywa(Kj8JW zwYEkNANEcfZ_ht9MY9mmgwa0TMI$1l8|n5AUBWaaheeDmq!Y9WAZfTUviMe~zMSUw`TKhR*IWMFpm5156aus*=KjV_nfiYqQ5cT}cc? zxWju!W3D=TfSmDoc$a;v9HoRJt71aNWgfY#x}m~m=lzo8>JAjcbHWJ-4>N(@_FYY8 z&<}+gCIRLxO}gra_|AbOPK^&1lor+(%-g|6_W3nXScqz^iz`7?`%x&!*tB)KX9`M(U7#?YDzqqdEUmLgn zaNg`Zk6?)znP zu>F`D{4FLWj%S=3bDgnGMbgKQ9b5nWEUjT)G->18v)#ci`>ymiKVYMCu1JDr{KG1m-hDTch!@fU?9yfE zt%r_mY-+i2!kGV>H(QEhy$Gn{;VnD1zU* zI;g&XW`YU8M@dPn95LdK@BT2EjAQ$iqWpe;du!VtfA@>VgAKQR_VZ&WOhh<|R3tyr zA_Ae)8M0-PF-fA7?8{E;3}w}lhcwf>jkttUx8y7;FfdCh9*>8^pj?Q%c5WLrcI=^s zgB;6V|A|kPl$G_YIwEoXJ1tvf!WxNsl$C0sYL?1&mX>Vu5jrDqMdQcq z)KOb^B;@sis!Mo;m-ik3WBRuj&6`K8jk4MXg)y%taLEmeA@ZLM`u39;F?gd}F zQrI0L3`S}Gm-f~+g6jD`{7X&am^YTJIzd!X%P199bwM0YGEF(;cqEf2m6v|>2 z7llUXs-g;<*HOf&swO5CzNoCAu72m?L&Nezd7XebGP@$AG0;3~q&o7)F+b5PyiZl>9 z814iw+p}}~vrj(S*w9#BQFi?&ZW%jaGJF^g|9|$r12B%WOndsw&UUSO@0Kmea*2EI zc5J7UkP0D34RCk-1h~s_aDh9xJMK7o=m{jGkU*MKUE|)nWm~eX-nG)!=`;WL&Frpr zSKF-E2>}hlu~)mZGvB=L_x9)M2Z01bvXG)KrNqazb|-mHhzSl>AJRp}aaPUnF2}EA zCK^F8l*o%HN*aWT?=iYaG)fU;g$UY25oDsyd#DddJKA-`Aw`xHiXydCDxpP2VPey# zDvYmj)H5=Cq4^B3`^JTm6ir~n@>OdcPN%URU^^$MgX5&MIRnZ2Hy>ns3y-WBZQ7 z2X~8{aMyhwTfTa2Ts^McO2sHM=@@s>YE?BN(AEGEkFIv1;Hgn{moY-}x=9>dZM3ZXB-WvZ#Xc_7qy zqEw9nLetL9?$=)V)5YdypMMlXVJ*4&?5V|zSIwR~52%FR&aMHK(5LFIt{TV(MG$VL zDAiF6IZh-{NmICB8X8nMhbs!erm+@Sqcks?-_U$x>(*`N_RiX};%{tRi-?ly^G81( zU?=aG*?^Fw=Dk$&`3{earfqeyz-Nhhc>HW31&cBILB#mMuZj4ie z^aQ>GTmuNN-NVDb`{j>+{`gNFPRGMv{4$E8ufO(Z{e%j*#u+o`WM}8Q)gDAiQX*|nVmn@;Xp-wuZ*g(iiE_C zE5Fx_la?+nO86G=%z^=LQAz3N|KTfO@pKGU=qZ2qu}ABtH(9N=IOU#5IWT3ZF2Ohp zlIa&2L(-v5U6vX?{xSck_t=5`;PuL)f)MFb+<63n4{%4D&$shZ`K`BR&73*f-xu_A zq=<KT>U6IZqPwfvWMqr?K$9A3zcoJWf#(#L?lLoZ^DQ82IQr?!N!`zx~aU z<*OaetPe*rMAd+yoco2(?F$5soj4BLUGwwsunQIeLlI@=cx&5b_ejxQAGOb#HPqif z>h}750i1#jQ4*xy6?OcvP_Y8Sq(F8B%3?9wfJ>&kd4^+yBRCxP)bkU;K(Kad-T(gN z=Zc^vz^W$EELS!ZVMV@HZFkWC_t)fXuvnw=dc_7yzs0FqEJQ<6H_3Ph5!IsrzCl8O zpPzVy=!36I0IdMQv?K^PK?yt$!fOzi0GE00f<-P@PEm2mhbTh`}Y@TS3f^{`qcUJC#Fnkn7eSvUH9A@lXUtp9Eu>=Yip~PEo~hb z@c8^Cx!GD6(%SfQ*z3z&kyz>bnFIMORTws0lShoZXTPeOErF!VAKIo zNrG~^^80#wfA^bTv|K!Q|HnUJHd{H)|HAXn|LA+)`{I|sT3B2H!bcEfh$3fZEr0pl z_ABL=_^n zW9c0UB)5kuN@xT|$LkD)J&|ydx^V7Xe^1{dU;Adw)H)o)!7ey^!Ge$7esf<>4^RoX z8KSr2@*<_)UeH_k| zg#3s2&<|I^mm(Ist+=$*Vuh^Wx(`R@(D_sA>vQuVjR>$Lg+-Nq&oCDpMKo-@2cKF` z^loi5xsxr8zauzUAjkwur4RP{dL=w~0B{I_)qh$eXQt9<4x(!h` zapTXgyO)_k?;!g0QUvxf(Z{e1TDdYo8qmAek}~DssZNb(A{>}WT1Nvf>rKhwQ=64of=-^vsh7EG)2?%gxu^_S;lE2=u^3xHecvY17v z2@0GP(RXB#@u_oZfV(3@`QAcsX zz*)IKz~}QqLJE>%7!yh1yLY_P+u&{A@Q*})p2CE2ZWKYv6MAFD-IMuIHRizbT zEJ~5UT_<2`fIEvS&`A0Y8t^tuAS2B?23JA6N~Du@u;x4`j+>H2(acAP(g9Q=Y4dSt zBJ?px`UE?qWMCW|PM54GmoK#%C-Hc_IeA6H!$V?kuiatud%f^_aZ**{p<2VJc*!%_ zICMNRx7T_Z3PEKoqr?n~>UidAy!Au0?8=I+ii)BBJ_w|%C`10Bt)xhW%BjJs5VV@` z5l9m3iZBv_x;9{hUZe(^27+mu&6?%P%64Tr9S$qBqO{>kMeo4@Nel!6x%v6`KlmvW z$7zacxp3j&zCCZh{R)BLx7>E;g2hXx)i3~~c@q0|s$Q$c;yNhfl!SPHy9}e;3|R)IVEx8h zR<7L;dDu7sRn!esLJ+{7S5m5jbjW<78AT*r=fzb|{At?6F=4~0C*D$CZk{^Ld#MG{ zL7ao4L=+TiU^|b3GSMQ|Uq2vJCqbG_jKvI;!DhGHoxmP+gsmiZ@U8Cxn${znX~52nmsqKun!iF5eUZ#1cj4vVdOYLA|SZt2hmOspQtPk4EDBP zzU1?Jjcp8Uy@J5seA``Soz_v{1EcO+SvHGf$p6 zam-?|&X_f4#_TywGiOKM=5Y`DJZ`fs1G;1_GmPkr0m9Nbsu4FtyP7TN>WV8dJx*xI zxk^;wL*pDLh(?--KI?kN^;FQu9o(~P>sxOQy9bT&Kev0>;~i<3K6850ELj%%`#WVZ znC;3>!3y^t>m%3ho{eES_=m-2!_AfpUA?k~IJ2ByR~E_h7%v!ifx)I43`a1q?`7v? zxtz`{csQ+AD^LZX2?T_QKn=W))G`dLi^t2I4rGcG zz#U>4v(xD;uPn~Z&aqgn3{6AFARHiR8j=arfZykxQdjqzr~kNj*NzvT`@^1HJ7&$9 z-`LbxRb7{tUkFZ11054h=ww~us#HQ0LV3OM;*1k1GET`^h+>5wPY?yc0+P==48FqQ z%GIg;LPu^v{$v0A<#ii2mzGyX9=sx}N#Pihr^!PdzA3j^f?;*YR>k15API_ri;K7-si3$Hq@?IL;w12P^73-?bMt_OC@9VY zu2UqB6jiTF>vkeC40U0vpuqEU7A%^-c-g6A$KHPZ)vx`(FV<8|S+(}YB`a4|R@LO> zl=?ga+9Yyd<|=_FN03)!C7nzp1sc*PkiZF~gvCDaI_t18jE(0xS(1$l0CL~e)y7F2 z7VakpK87qReh-)IiE5Yv2P#l<{2O4YE;Z~APymd$+;S!u^nnc?M=57kzAej6qv;bb zz!V5!xD{2^Xodjs!1K>OU077SZo@_&%c3@0kmpd6xYX0l6qk#783ahb%jGIBD=8~2 zv)k+>K~OXe*;RCizpFEMs0N%bpk0Fl16y8xVPs?!wzJ`w)!}&PGY`M>>Xz^Q=f6Gw z>=WPo_y1YEd=*MCn#5g0ID!ea{e5U=UBOeKu&W-7CYDLLI;Mo_PF~`iSuW4W z=yOj!d2ru8#$*B~>F@7uYiqsz&ig7VtHaWUkThX)n8#a*NYiNIG`0X#g}|`XSz5%z znt%7i?^xE_H0y?o&8NK{&#Ltsm#tixzAk~GCf0=M^LMh~2VpgWl5LkR;m{2u7@HHd zmf>Xg#r0;}DH6Qm*p^e%9y|G?HNvUqh+*eYy)3Iu!? zFE$VM4G{zpWo(PGvhy4+yTuOM>^ZMj66I7PsgLXu6qZ)b4cnMWsV?S;K^7u35et!) zVT`E2Jx)$dsX?ShqtT4LSdG)oa6>?5b&2bG$g=EoIM%FN2ZCu3Q$hwHMaj)|6qZ)8 ztU2w+)1oyJIF@3B2)=;^--7WRzi`R28#mv)VZ+*&UU=@UEw8LtwJs1C6+x)uaCwFX z9N7f~NevAT1U(}lcqM3-AV`Njk2F~Z`r7$mu(WcjF;){%QH7=i3kx9)RjtM2nXMw4 zA~fAFw|2^u+`N3>NWY@U7|sq32Z~DayV@@1<`%J5EAZYF-mlj>G8n(a(4f!f^LW7a z@z{61Z`5059eg?j(5e5IrWr{H03*hvE6InZe6%zf9Iz9ivI^T_($5=>-G!p6*DPIw zBC4z<#6FD!EYuRgp8*+@1SyT=d?5M&TqE!3u$6UAsc)K6kDNGi^!)iV#l^)nQybDl zR$aTRFuXO^tal2LG)=Sm2(;SLa`Bbto&jO-?74H+ZruE*Xa8{i;#r1<&ASViteiRf zhTfizmwxwDPe)sBLBZ&YTke18A(pk{IBB4k9yqY~%@S(|YNtQ2(r!Q9az?6e z$j;98d%Z6_`~38oH{=x*{@{DxUAl7Z=v=k?yp7dpDSMFqR% zw%dURBLu{OktA{>!nM*otN*v|KlV@ z{pp40jvYCYlUD>(E7#>bePaK~a~BpaUorI4AIVOq+3DJ}X_L)F4!Z|sLFn#m1N%W< ze$nXY2tyOAS1tAW`2AT2j_%u0Q&UBoSXB%l85jznIcBTP;>fvlspYr7{BdtjF9_X6 z274DQS@o~~^req~`r+j(*65VVaEwzD1kyxao6frlJ#;h$g&KweuPV)KCL9b8HM}4R zc*_!0A;!N3?;gk#hGXdn&9L{nKoe65@I>tnzt_L@tu3;ueC1!hSvzg&)cX3HZ@%O4 zAO3I4#S8E4^cj6JgfODWHFXX1mabmCZbMaNX=}^*?!G>nB#UxgHB;)cT-g^+9nQ_m zX=s{RQd(Z$Fr8t{M~@!uxN@bUsh*{B?e6L7(DM_}h1b+;%53&w z&F5+vrZ7TWr^yc_$xk#vYO?nvTT*D0D4OXX=p7jByXE$eu3mRzSyg4j^yv?M_VXef zukpQak3bZ~(u%4n^)u!#T{C;me6QDc<{eyj zY_%App)^H%M}6Qts;cA{7O*Tc(9>gf<})t)>sS)8)|uD)QI{lbKQ|5SmFTD=()_%(uSrPZbqaU@Lk44^JLH zevDyQyTdst+!Is@h$NKUJJb|Bc}7b|`xBDmhr!N;1i)vo%qJ>5C*;A>HHan zB1O*E*4Acs*sH3mM~8+;lht_J{KZR$hWd{m+;au!!|))uyqc+vD2BFNIJ@=Dw=5Pq z5Dczdw;lu%6ozmdS5w#2Fyq4Rcem9|odT?Z#YAheAWOoWg^S*J?axg!W&t6Fz3r%G zW@uwIfO|`^HW)F>df7Cy&L0hSn@c3afdA~@y|eAoh3QQ*-rDj~fD=CT*)J9r7DrTH ze{&jCW}`7ZZFv1&P*2k)rqX|RsyuSKVt*2+WBkVmf+AAXB@{Vnvj(g%sW1Aez?)}@}x^m^p zoCS+#+%PZ51zg##k9_2=ZQFKoLGSW4>uaiOR9Tued#1(i1Rl|hrY44>JwAVKVab{` zYYB=$RM~1aXE|LZMa5yV{H7Bo2fmNRqS65vTH5*HzKOqcctH=qEUW-;4t zy5-J0KYA~?Uy7mxK`1V(D9A60wuX>HY}~+8yq67uVbZFE(Xs%yn@qrc#;IJQ%&#nG zq7f=YtdLU(YFhGQy_-SbNYwFue9UQ4_dt76MzrKQtm6nt!60w5S(D?Ap~+$iP7az_ z2TeIfh6hJS+Zjp(gQsm-jRX)qN1y1NB|*$#bO4Umg>o2 zFd@gXW{e>8kv0^#O~GJ*#7UFIo;)$2j12XHXgI&PjD)nS@KZ)0!Lk-$&3G;VyIUEH z0*68rl0ZdCbr1IKJFu_6t7rA<)#cTdL5`z=o?w_P3c#;2F(!&6q3_7~6e&E3m~PVp zqHc4z5R~ff>I9x=v`m1@sHm)B%@)q@9UkmaRhcodDd6EWy@Y}yX%~4c^PAp|X?XPc zQ6jNQh=G9F;dJE|<4Ic6!G-cM4s6fC!Iy(CLJNstLo)e?r*Bsgm zT0l~~r&dL+A$cdbvF*{Pwz36bLV0RY-evgLJs6O9le3QrL)!zZZ1_BU0I+ROg zDf-yaBS#MI+q~)alJW|H^Mjqr?-yl3G+E3ja3OhMGdb|Yz#`yurj;UK8%<{0iDSnf z{qn=zyXy`UUGQfQVTa60)p2+dw^W;}jm#zZW=8|Mjo`bnCtMmsM27toXG9Bb&e`m23i1%LG;_{8q5@Ksawi z(;d8wiU(-SUZ!yxPZBgmq?^6|K)UI19QU{fXvSJlQc)1q;a^c&vS#`G6>C-(mzKSc zS3}{H$vZsI)zhP5xSb}&+`{bqq70TAB9J}#^B>;&(Fbx0iv*w&QNN1(2{9vOSrY`k za*7c&1G`sLKk#-YoM(oJiGsk31J3NC%G%oKU)^-qN002=J!8h~nkm!%#&bv7Rklqyx%FZodt#+A~!5e7ZLV`Dlf?vBzwE*$8A_xc*gZ_Wx zhD|n$-R{UjCXb0FVIk$NI)TPv19S!0Zg~RVs?XyCsY)ONjWcJ#ED09DVo+rzB}Z*C zjZN7q8{Ic*fHVXG0jJBge$!@==L13Rr5Bz)cH+d$88d+2&swbk&nQaJti_6J>Tpk| z-Qlu1a*!azuyQz^5Jv!P{w(e8>)<(mc3u%%*rhUYBM@K#EWQtK{kq^Fw8*I%!~$%uPqjb#SZHaEv*-fSOT1+yS@F5 zEw7>kv3A4enA@B@GE@nYf|Nf0OSEx_qdaZ?dG zJV>ufGKy7@Rn0`h`5we#15%cqTiDy(U0hK7*4wWQ^mNrst>3WemgdvPUVr&z0z()E zuWp#VZ21acV4ry6i3k4fb7fiiZ@>0pYsX`VE_>);DytU3lWRzbPs#yl}4h z)_Wc($;%$>YVYs3)aS5qjg9?71FyaKyxC^<4-e;+l&@WXBTlo5#AVXs1TLFpEoMjV zrIw4||F3TyK74?pNMp%VNC(5c@%o>C_Uk{aS+@~MNs4eanSJDMWHyfg4*)h=U**kW zP*~yaN;AX+!ZwHskEcgTG2qE4n!4H~tR&08OTqA@iifMjSl(V|kR=E#5ab8@yERGb z9vs=bYsV)){aNtwbIr|5mn@qxYff)Z@1cVSa|#OPEm(Br(uHmByg8#`nmwneqoV^z z-1+9SZS5UX8)sJ5Oxga{D{sH~MtNn`TU)kN)lQq))a0<*noplBDcD$0Qr$Z+QczmO znAjux_Y4gU-29O*jtup``|ci25E>K7H!MkwXW*{@8aTTS;eE z$Df~n`mTFFK6P57$a6a2Kk&*@XK7HK-6k!?TBwa613FeTuI~X)h)ttqT=g>wb*Ve{ zGU3O_1lnwj!+ZjP3kp0CYyzna@c2lYp>!v>r>k?%?wu=FuFKAYiKf#hj}8pbiJ}BWl5V&A!2Uyu$b%2BU%$4le)?elkjui}Fn4}Q zX~mAWUJdv?Yd76eU0r7~o6nwWK7XNQeqN~#=uMqHc4)A#m!&D0_j!kUF@p8_e5X&I z{Py>LU}4#cva+`Jww(Ne?%p2O>aaMoO=g=TJJ)P=967MBu%v9o%2j5wIYcm#8IF*u z!CBLW#_5lJ=X+~!ygB+eIZoHA^_%PtN6_cd$;}|uNXo~gLGD2%y^SycY(+s#J6aKa z4~8-(oCcN!fnw5goGYRTtZz(xecb8dDihU-vkj=~}5=;#R{C=<#gUd!#h!_V#*#5!c2(Yow>m>=o zY_R~t)Hq{aOL#k9qMD{@NiXWDkRJ*uE~{9+9O-Dk+|$#;`@LCC zyVc=BR7#c6w6UGemnU%nUv+I=b?ubhJGY)ac>*kx)nZ+?YW3#Z?lcgwj6IPcP?9E6 zS_y4D#AuQ)R>8wd%cc(XGI~}a8MUmIX+sAZYb6p&UEC|Mf)wk5LOH@?Xu06X@OYJw zr4-nyD2At&BqS3QkA!yLVzmRkK73$LXUCOKfByTf+?>I|fyaOH!y9k9wX&k>_rLvR zUeVU#vWHlUMHaaCVQv=)D+rXQ&zP-GHR6#+ri0zX+sH4exOAoM!r9Y>d8_*e1}?Q; zn0Z4}Si_b4vZ~&Jf!b*e_0wl7G9Tn+_t0QYUjE@j`+?Kda^b?I*0#mVSEG7@9QbLn z$aP(5;Y8u4TkpEqdih_z{Du1ZDb6gHh7+=?WTFzlNDw&T_xa!5{>JNDUh%j`jvYHZ zf5DPt$BusfA0Mf%t_>M;*k+58WNP5UOyyE%hmn9Qd(BvgWGW-HKM?Sl#wMb*u;oea zH=)okkDg=IfJ%rsbduA!TZAYo$pubJ1^|bKrU!wrvSIp zWVRmKzweLFKD%b)=2iFc;CnV&Xt`LwWp>*GtFFxk!#9A zq-9*Hi+l5UiHHLYhW(T%5xru1(;P;oovMR?p^4R0Gbnr$Um=`1gljGENt3WTF%v*KWA!!0xTb59}?hsJr#H&rqZp60O5|UeU~&H{NplzU^c1Og;UG&Ifp+P{A5_rLt{K!0yUCH^*# zIEu$edvRJNBxoi@0h>(e%K{r3gduFwy=Hvd;h4N~T%ZVSmH9<4{qgCgD^{O9dAjA& zrLWJOLz3kAvuED{>alTSc6RQ_P#?!}7=~Y^K0s$?+>SezMu_r;VM@mD^Gu&LS5Y;u z$3sIX`q1IX&Rex+LzE>Um#*BfaPcw`_VdwPz}s=RSW>Xp;z@NJrb1z1z1ImzFu4uE`3+1e353ax2j@NbIXCT;RZd|n}%a#4*uYEJWxU#Fe;~&5H(ERyJZoBI~*eEJ;U~j$JyYl-3 zAczeSPlv9|XpB*HVjzv@g{XVt2n^!`1QaS&qOwwTSV@uyJUWRN%{FV#m6qTB{;B1w z*4vy}e!pMj{Q64ZI?a|6h7gIY0B}&jw?pb{I1?u;D2|)Kw+Dlvg^~I2N@l{Uis-d- zt*WxBp|QTD^?PX( zMq1}AGa3ZJAqoLCid$OK8!S`XSV0hR3};LhoHk~OP`}5k!=y*QF)`#Kc#pt|Hb;&< zJ6j0)7_%LCqOBJ$jQU5LX3S=2CK9u8Tu>Bzs45}Xf`tf#*F4W(Z6|CB=hpA@(;bjM-$O_y9LD)MK+b99h{EWA5qeJ9qAsE6dd|b*d=F)QKXDyb$vbx3T7= zWQQWp0c0(k(jD5rd&oV!e$!1tsQzsrxdgS?G!d#3P$UIXfhwV37;3dyFx=GJ(|_{h zv6ZV=Sse}qB6KJbn!jXv1*wKuhGtD*%{x1Lc=?LX)1xws=~C-O=PhKs^$FYAN=3{oH}+GO258Hz%6;@l^1^X(;t5I zU;b^*yhRv}2|3O86iT42Y^KJlr-Y3ZfDr3Zf7sPGU@7Z|{iDKfSRjk?~PS-EPldH?B#T3Yn@U zSrYglMX`|n0MeLIIu9MEs0vLo2*Com*Yp_+h|`1PV1-B^9T?EPfTB>W*#Y)lG3f4W zJ)^Byh7%Sm&dof(*v7)bzknW&PWNjy)R*z_rKUcg`{8Ky&^Y1)JTa zz!|rczyeWt1t^a4fsyTRzj?>qA1|+{1a8CAzkPh?j;$c@r76+`+$npu#cJ#A?sw%C zx(J{V#87V^j;Xo1xvGYBcXk{(bg-|ttGcwrWOM3fL>lTF%FZtcFRBXB?g^rI&|Onm z5)4WRM%c_&uzU9Q_gk#62h}ghAatP^i!93By}h|E2W2v2DCQX+G_huZQ@jFab~q`L z8tUzo6ip#$MV0~qzrgwZUXKpQK!HoslFZlXgF%qbh-9pVCB=XD@ZW<7XS_*=SWjk4 zUO^Er1XWx^ab?^-j*&POUH?j@2VutvMDnaTmXs_}S}{%~w7BF?*ojrdjOr2uMbb1C zvNL2x&}Q&i#a@Cei;&3xBU69l2iYl!A!NC`eYM)(#6FkPMdAY)af=I zV*-URmRF~0iV*DW?k*`TFhb1xAN&kB3-I>9KFYEzPLO|k_IVHt-1d>XM?E83UU>mg zu|U96R$0Ab^;#}4dan7j$KyHEd~WXi#XWsJFaPmru>X&acvi1lKV$ZM1;x%ZpVyxJ zjaN{%ct=0`g)cf?1@7UYmtK5MhkO>Bnr5zAw;71`vE#?~?%GydTG82ldBZKYH`X^D z*tNB#r6uU~ELyg*A*bl|SO466`dE2+IUfXZxn{9B8HNEGt4}1u2J_^Tx`?c4B5+}Y z0cWGPIGi+& zj^_tTR1Syqg#@J73gcPmx}Aal1ZkScP^k_@({oTEMKPUvFqH6JuxJ^$YML=m__Q=@zwwqkp8w+?@^iAMG|ZSiXCZj8?Q$ytQJB>8 z=g;lmv-4x0ei-&qcXyafmioq~kc!?WM-J{^x9MiP-FD{KAp{ws z8J4wJqe?faR00CX%5vI_E!c&D18jF@V~K3Orf;08k_s6SR_=kP5)~=MjU4LpA!4Gu zW#W^Hq9VX_Uq~7?MWZM>btx8@O`mTx$oV2~gU#C*fhC6&fdk@1OhyNy$#A?zt1^zp z=qC|Bjr~EY1h4?C#TNIX&1!$I#s??vyyt;!@4Ojw_a8m5udTIZ$?}yJs~yNF@SwmZ zapUHj8X6&(Qc*#HXK;A9w<`!Kz=nqh25gR;ii)a{{*I!elEJ>7bEi&Lm6ePR_LvAX z5DfJ94=@B?Szb1K!BPZ*9{7(RK3G#-*Vc0Jq0c-#rM_v#Z29S5|K#0W+iM$Vm6w#X zwO;9HJ%?#Z&y`l-+kmG~TRU~bO&_5MeDAwETu#S|m1~R*b+D%+t}htF$<)T3AjnKs z*krbv%6B6QZNzI-C1g%|H=AD5#9%s{5-uL_QD{#0@vz1pa>f(7``H_asdb z%T{kxb=n@Vw`Aw!8}Gc_+H$ewycztOqIIfiQs2fzqiFB-c)$kc_XqpBJ8d>=U3qnW zQSnf3hnTr-Wj1ft=F;T;AFBc4+J$-6mU@$MoAwkwa+5k2- zF(wq^rop}0mX&2}!!Er#KCz@`N89D&M-JaGZ$2<<*UP_!EMlpASK4$Pr3e@w5dX1> zFjHVVW^(fa-Al00Ky9M3qDV|j2qc8i!?3?lNF@D9H4(yMFMZY(daoH=*i_8sr^ z^$*CBV6|G;gCN^vdj6TG3(Cr~T{(aF!|$ihn)A^IKLw=!k5B$mh8sC_mqndN#+B^? zYdC-T+Un|>P%7Ey^ZNt6{R4u)1IMMM`K%+iASXYU7lfYP?#k*K_fX%kd#JR$tgpZK z<=0;R>ZAWQwQkzcg9m!rFY43`u=xWxJdBB=SyNAQ4~YJ0ifV1WIDxT2<7jWY^4nkh zxaH!R`i7?L+``iGiab3N@V9W_B%S&VV5_69-UBCses5H90kX#$RxCc|F$nIHC?)(* z0>@4>W;owhXADIt#^N6AKe&H4h#q_U`naHX_6>8v^2xGH(e%9eix2JDvGuLjZ@=TC zjSbTgSv-64z?I8w?Var<73JUzCe|F_rQXiAf|ByZOPB52yHk|~J}59WnUkA~;$+Xj z(3vyG5y5}u;)RWO+?}0QID6LYmW$^cHZzD=%r?iog-dW8uc@wXzjPjveEaw8g7jrz zvn6O?yLA#rtg5!|uj7of5F%O?LXdm?d5-Z zD{Yq#?%LYd+r9agTW^@R*cb5g0`JVq&dtrMt(|iG(BYce>V=D!wqH1V^2Eu?x`xuq zN)u}>EvvBFZQVUxgG0kp>OinPr@O1|(5|ih1N}gVbMx{DipslS$4Z4li7yJBW70}8ElVieqnuGZdAecb}kH948Hyb^W^(G>jb! z|E`-mgWWMsCxH&4SJO2RW^RrhUkr`Ky?>L+ogHnDJ^IM!zVwyp(`TWm`or)4_mjVV zeAet6zWV65YU}DJrTpp~r)WBEBfkbYW)O|RnV1?4E>RRqQDkI}3@Q&7)9#|T`hfq5 z(J~sYffLcB^gs~3qa$)U2O2naBg6exr!6576A(Er=rMSf#_xqVt&@GJst^pyf&kOn z6rAA((K8WF&w*Qj3>+i~xnXY>P*_|gCabnSXWIZQ*`tLUc_OE8TJlp7Bi}+-Xu}QaTLhy zSep!rfENWpKuMsoIM6Oh5+E{C@{kM!$A0f97xa^+F}^l4W8N25WV2p5K64;Y*> zRTK0bhnv>HnlOwRvV9wNMoSQ#fp`SsE+gPZNQ*s7Rk3rYPQ38!Q>RXz9O&ttvtaSp zzxkbprY6>61zUi=N%6p%$30~XcE6F6(w{8BeO+Q);{iv>eT;VlkpeI*qr9UIW1DQ& zEJ6%}#TG&1Xk&mS0A8LxWe&D!Tn*bC!(jyK3-kdBkcx*5ZFwGQ zKS_aeqY%6hwx|A=S<{?X%4l$4dRW{WF3du(e628IW^`G7wT&Jv37G2fUapb9{%cZ+4B#?_pI{1WsuvpNMHVv|Wg#Vk$|IC_ZkXRD{$>pllS)=m`EdeE(;^DzB)F0O@vi zb@X(1*45R6*cw6~XfiMfENh8|BM1!~jx{DDvIG|yd}Gw@!!a)$qIQPnwqZkutjdBy zv!u~d5%3Sg*-O=6qS9g)36IWOf;ad^hLCX%u!5-$XRh6un{-MHtPdaP>um>~A^0$b z0SA~tf$dG!V5(`fi3Lw0S#xUWJ>VW68L66?Wtw<8b z`CQpK#z1LD$CY=ty>(#!0k3Cd-R4{Brq)NoHjFToP>pvwqpnI4U`>WGF=|v(o+v{r z2lT{<<-pZQqJn6G$;280r?W!<-%J9sK#JDbeRa z{3ZkfCbJ16Ai##C@|cu-BSul*h#zdHD4vx01YD<29o8}aLzQg>)$njr)&yaawGe#{ zu<9dKj%6BE1e%=?24NyS45=cV8^P1pnh4Tju>q3{(NI+dO+GdO47gtkz{LSB zYtqqssLYY(X>d+WRq(LUcu$Vgdod6CoCc(7xZ1hu>!A&y_0`~{0-lE9PE%1idaXee zVYNHKGb145G{yz|f)Kdg1xf?AlcG(1J?EeN>aGVKTDD^KHU6RyW|9`I zu4Au^d5Iw#8EyI!Y<%NvCXir7kuuqWjeQ5zM|@Imqc{oMU1^3)vF)Pa0ei@YEy zA`e?0@v9J?bg`|br9-xha$KQzpcjSJ4^kyUI|PCt=!Xn>SCfE?jSwa(q&_Hav1PyV z%8M79&+;7C(t2t6sCmQTAjgIunoxh}^C!yl<4E%m&O<(~Q}(X3zQT7ykb6fdlU0 zVO3SGhdLM>W=dulm{+l+*CA6fag~Tplj%B(L~v=Mo){YAb6>w(`Op{(usj5U(*%a@ zKXkCHybQSY!1flGmNuXBo^L)|UstD!L47jrx_NsjMza=zF^#&1&YV1a_U!q=q22`x zRx|<4n7`=y+%$|HKLqX=3X{F6HbDU;1kP_TRqFG;1oW#^I69^&a(I(7%$uqcEr%;& z`UuBZ^c9J{jO|m?{+iVRc1GbOWpWP>jf@0Z&o>7GTt|C{ETO}L!z1qD^JmXlOg6jS zWOL;3oag#ngF>+&s?Q%D9Qe`y{^yog{xoOaf<3!;Y<=@>hr{+iKmKK9RV|ACjqX{H zrvXic<&n;D63!aABW@Il9?6J=hXidmUKRA8vD6Dj5Hu_ET6`f*Q$^B(fS++V3N%9- zI2w|u3A{{EA5uf3;Up^tv&hzHAh z9qtlRPU@g#2lwwib>jHWy(f4{SifTK+wUIyk8geb)jz*@_x%t4?F>#_<5(iQLX6J0 zC9p!+kQfBwjmG%Bz&0tE#El0_fO%?JM5VxZ5GZ7{A2oQVVE>OKV6HL^rBnI_DjFOR zL|moKN&RwFMM)FS)6p6zDYB@Dl*~YOYnmh|991DRJZ$a2@Td-7y{7|OLH%_U%S?R1 z=b%5`JJ6*l7{^0|GJ=2&gg`wc2?XFM$4UJ|yh{k3W-o= z*_JEGB7tJ=WPzD1A`*|GVobV5r&@v30kCl6Mr9!s69eJ*N3)p2bcc#l7MY|tNHHOi zCI^C;QfnzDdr*l%nY>FK3izIX`P!#iT8_QF?QCyfa7vB+wwu=vjcAYm{F(M{_%$2{ zu0mr>RJh#=B76b2E21PS%*`u6AtQ8^`r8zvAuEc_AU?rwls-n>?u!@C8^0Dsare%x zPd)ML#j932UD=Z%-$XM`TKc?4Vy6^C-dm9koK(~_K9z4kqLyq-Q&VBfX%K)OLB|=Z za1o?v!*w;@pR6$9gmoTD{(?qdGuyrV6m? zjF32yVhvRkQw@Y5b*!}n{2yp31$EURY5*tF^aigeUDey0Fci^ZIIp56ZIV7-?Jrg( z-a`W7J>L|X1`f?2P|K{XFxiV~H%ZtP3VQ^MPe>^0i|Gu6e7+J!Fh<4&MLCmIAW6S} zkjM3@+}}U*&I9+ZTfctA>QzfkEWKmrk$1KpF|#K9{X$lu8{C8}YVo=nd!4fAi@DO+ zcfmd%k%<>6RSSBN(z3eS@2a3li2U-O|Mw)%^HsHVu57E!!{;N2n4zoFOH&wuAExh^fWlV7J7g5ey^nXgcjCkSVaEI zPw%|<)M47tAd+O)^j^l;Uy~#~8g&}5BjSoGc6WA-di)ec^!2-c@#~i>D#}bIi`U~m zcm8r;zX!*$;o%XFyVstDjZ0o9&{IVbQ!j!dO5SBB(pz{?ENOLsdlh-FJK$k}QwdE0rkSi7!vbkT z10GT0fQt&-QdNnN1X6(=!obbeSSMnlSrL46Pz9=}#ooE1ppr-$EQhiz6R>|iMPC_s zB8P&f&k*1-k4HK)X;4hcd!vipS9ZmFs-aj>e$X+|~zqa=XJ5NlHz^$%`;PHR9dXr3LQJjVyJ|KMlO|N7z{h1B_K0Vp{k(S z1a-yc93V(Hl5oZb?iUziK_U1F2b=Q=jm02UqZkDsic7pF+4iW1xSA+ImFE>2Rn(^^ zmEtN7V1j|`-=s=D*espMF2V6t3V0*n2^~QY>_c%^aW(`+EML0k>!WVrOaJje!>oD) z@$14*`n=w!e)_6s$oCK5et=iE2SU4miM2>USXVP9H=wl-k8r@gTrMfSQzR=0LD6|^(n?I= z0Glpw{kP)49VSzRDyj~VX8N$)Rh+Q_`*)BR-g@i8()DMiG}I9kX5{R;JNlpc;VZxZ zJ^ZitFbo3(6+s-Iv zclvN^r4D}6b!m9VO9vONn0aXLDFZ>lH!8gJ)Q(dJL7-2TPb*n-^Zd%%QUl}#CqQ&U zJoocA7OtAx*4hrFcfqQeCf0uCd}m)z7i(r~8!EH%>_(0#;1BwJLEfhT`L|~=R)+;< zr8Uj#_4&O)h&aJAHmB7H%A-sK7W4=9ynUjsvHbAPGavo*jdq6}T$R`32a<2GvBo5a zAWDj?GAs>Lh35rHfGtblgs_8YT#v=1N=zP!j3A1&ceD--y8qv|KDFifcg`N}zv(_8 zJ<{b&+ornP?m=x)C*6({Pv6c0-iv?&&^xj z_=RuWlUG>e9vb}J4_|%v?ZZBguVL<#&pvX0O+yJ#tth3yMuxp_zOd(!?|kClANg6& zmENL~91L|l`?D7jRQ$x3Zzph*&*wd{|11}fSKm5c64m`%Pqw%ADvET&;u-a`Yj7gI zw*Z@b{F6tLpc6wiR95-ghH{kf4fI~>xO{p4f$i(=SZ;Be^yCuO-F4;2p>u`BIWCty zrzk7G!qs}QqpPbu8njE`#ZUgfTYvEUKW)7G)@`pI-oNeCSHJhkZ~fw*|Km6RFm+bd zPagXN*t7dOJ6`_X+xLC;mOmYS<{Ll#oO{p<)B_l**Pq+Hf5-8ky!hxFr=Dae=1)() z(bLg~hBLN6Zv{Sh;Y6#+OfOnDbIyW>(?>74-99(~%b=~zT`gxiK$OzoGyK#~x5yB= z2l>_a|M<+$Ut`TQL6blJ_us$%+*TBc=V2qz>+MR3i_zO5hUYfI{;mqNd z(+65y`A%;?_YYsa?_A4XT_w;n2QN8owu`5)^mh$_C+r&u)J!cC1u16F)f=Qq3P>8) z*VUh2o;7D_1BOX_Sl743u_mxnlX3rbX6tVFLxi`cXrN- z>0Y-_2nr?D`DYJZa1V?Wm6b&D9PVMyk^SfAt!%k+S$>HvyNpCH;j#q7Vym4IIPqiuaBIWt21yQJ7{Zzvj3mw- zYc)RJ-g=4S_^N3Y1Y!4i`iJ^PoY{7Q$UzV{FhUla$vr#@G@_`i^s%3O{=lv?kACL+ zIErt+d+DaTR|b5+qx;V+WT{u4eb?RR_Io)E*ab3#yP-=BIXb`}e)sf}^>bc+>YbkM zp))612788TrgAmY8m*4q=g+iyM|&@x?w-GDCS%51&UJ#Q{i~n+4hVICd|ztrU3>FP zRf+H9CK%^*Gz91x+mwf>K{3=JuE2s6X2j>DCXygp3C4F7X($>P=JYZuI$kza#fQf0 zmTbx@x8_Zid2d3>T}f6{xgE!PHH}Op?kHtaaY9SZu!c}Hnla2&l*%JgbYmczAOlN8 z4Qiu*tvU%d&R~F(BpJi0im4@A{`fA4B0m4GcXOPl^<-!5)N-&iQ|sp9=*w^q-kgOX z74VitQNsF-`5Hv0kOY4Lfh>xGKeKa+a)$dy5ClANvxaz%>^omlo==eYnWGmRS=L+c zU%6z>lH&)D?RfnN#n5##E3@-ljW^USSTh3zAs~DwX)-&{p%*dqz&hYLb@;q*G{{g? z=jDFzeXbm5%ejvF*|l~@S#d?q`IDY)uk1Ot_splicDtg8K5vl3sn7lM-Oenh0{3Dp zzu3iE&6*ZdTp}oo19o$1|$3z`=dP73wWQN>v zK@Y;_JFq;I%1tPoz~Jy+-0zl*BJB8g1T{`?YCy+JJ*ulHq6D&ustfBX=?k)z#Mh z#`Evq^~ntjSFaQ$&-Pajw4QBSw02fSZRz3NXYT&YO+eaZsdKQ;t-(s15fD?oo$Y6j zHs5mps?B$=kQJH0h@5=K@%_znmk!z;#S2$A0*mp)4_?VFa88@is3BtG?CM{P`pT*b zra)qARF|uuhK$B-s{|x$PSPGnkz?)3qNq_MPE&-Myp(}aOp0JFG7M~}goxl8D(xtWIAP5#sljn3JYOruI)b>w_4r1DC`N_RFO!TVXmF$gL%29>LA~MDz;uB< z8YCMv**m7JVMaCr0X}MSiAPafAt(VOQowvc-ecXeU9H||u6*X9M{aiITM@)xRO)*0 z?{9{63Pd%Tsg*a+nYm;N#SjSQTl10m+4+vviRMOc@!rniJ3g_Rq!?iIrcAF`uyWQ2q9e%R6Ut!3WHW{b1}NH6R8f=SBmLbS-qFE= z;^HJQ$uKDbv5ggprs!yd3xS7lRSZkwkZCgnC?rafs%SLL7_FB)2VPJ}GNel2T%11$ zHldQrDx6HJ+nmoc*x5pv7-Jq^7KLQnEacD-f;btogFrt7MS-iJDyW931S11Gr&pAK zpA2E<6iwDq{-Mi95DG;|iRZe&I~9>&OqQH1h9Q$Z6IfgD^vRrC2MHXv??KRIB&1Q{ zKF|Fw1PC-(D4HP=3>ShNxKNWBtOLsn!=j(Gxm-YhKtwJE zNR1d_EoKl`a{(V^q6pacsu4IiiQ0a-?c&+4qSD-&X+$irBf zSqmCVE35Su0c7VJ$Ic#Y3338(iWaVFs;)01FvF47jvqYNeDd-=pSmfksRVuISFcT- zUNK|tGz766+ka@^){}Ss-Ho{gS$YK>yK=GnkH6ZYXlQo6eRR;jaP_Q3tLLc*7rkS= ze<~4U0dO3s1WB1wtHeN0hj(NkudpyVayTl9An1@Oii%SSRYhn9$8aAGDI*?B?y12k(h?7V#06*HmL_ZIOA{?Poac@1r%9UAjqpVU#FwS^{N1zhLYf3 zgJQsw!1d-o#1AwF62}L1yA8P*5yoml5JuuW5larfn>5+L^A|ZDT!knGH59TZt155Y z0*;#kzLCD}A&NpvD$Bsf71gItn*9xz>c${XLzr|boS}sj??_54tZ4T~g~6k$q(;ty zs1861<>+Z7R8xSkV~UK2`tosLD-aN!5fp@2hB*`j=r$?{f~u%MRxsR>*hj@ML4~6{ zBr50);YmVd*$)KJY^$?aEHKxp#KFQs*kcunD{w+ZU@j?=ft2&@LHx^s-`&Qdh%gr8ES3wv@?AndFgPzouc~ z$#@PYEG4CO6w`ceS>*k&3mHd9dQ31TtJfc#aw1XK%46yXN8G|#b>m~GF@4r2-LTXu zECJOLC4!+8NZziE5C{27R7Df!S&$ zETkqwM^h&w1)gbuBzV7M4!6FBZ%fCk0XpE9Fw`By#9^SDnyTZq8tD{0nPj+%VZiSQ zz1yTp{FPUrA*Q{CC!GkWJ3s?%WZPys%qiH0;FT6BR6Wx+4ma0j@o7`bB*!HF1-k(54xkJu2j z5L}JO%M=4!mt#^s(ASeOU?+g%2kgFn&66rId3;zr-f=LmT8wcp;sG4P6cVZt4q`-f z4ER;X6%3^?RF3gup>6|jP18e84b%J2CwrVGkI5sho-}z(9+OA3N@(h2C(z_EdBj_Z z_Z$5R)q$6^$-cb5s91b*3G{=|$wl!oCc|-Jsu}cX#_SV{sc1ASO-yDWbA1pM5g|~L zrY9f3UxYH~hb+s~@RM?_`s2T?Jh>R~|lQS7SiqMWhB0eO(IHV{t1Wv^X zi2NZzR6I#hG)^2R=>GA=Q5?fzZ7_PNF%+03NS7xns40!D1}B8WC5Qz6#8G@a*pW_n ztzeqYupRYVqh_SP8fxfJby`IP8)L|Zl~o9a6?zZ+PsZr;XsE09sx9Q?@gY!&{Njq} zf6EddM?J`M!BCHKh;0IuB}o>9xZ1lUaWZU8jgy>s>I+3f6-m)F6~iflBGb#BrUiTf zSg=goZUF(UR8`?|0*mTXk)RSGicCU13H3NZ-G)4~VcrZ;fFr!B66%8j`XF$8CPZPH zk7O>8R3LlBG2VTdcRL9zlN^%A`c#zN4VM42e?3eX{_0@Q;BGZs;Lns--Jns@p?6RTrZWl#`!F8P%)@gP~2!XL+8X|w}YPve>)_ZkmcxY z8by(4UgvPIV@y7v5-=Q!hT*Af7eSM(!=~~v4yK}ryvVDH60Q7t>pH5+@!bj#$UG>G zHK{@6fx)d9hJ}T*PZI1#g~K4@Lc36&1H*u=Cdl+~U12irRJnG;cp)j(EHu`Eh6g%O zG^P{nF;6T(xLC|C=&O_rk4voUngtiga zcwmf1y>T)J+sJWDXEYVelnG7AgNov?fk@;;g_4s5&k$LdKyew4^~%r=CrUY@Vc!l$ zchK^yXn1~KgPlUEjvuM2T8thVZbFoDLONaJouLk=zsMMDvd8H<9<(XGwHp{9mbH$5 zXXbRgu$x;5YPu02NK*}MWoW@9L5Q|*viSemyPh2cK{zZBVe8t~rT1y;(#v;nZ7pA+O%M<< z!O;{AJJ{;Lcjh3IkB{Gf+1NrzgrVaM_%I?eJIZ>nG8yDAy3zE3N_{J(-<}y2@ef$! z|5lP$f_Z+IzI`YKH)-2oG$Lcl3R0DloVV)INf6m=cl!fEt9KhH=_9{qtu~DxX%HZS zh^IESZ9xlNvXUKUfm?FoGz}>#0GOaiiHK#W<*sBuE;YpUlf4;q ztpqDZRB8PAHH|K40RarUbF`o|am@}QB7TYdcFnR2W96#|$;6OMqzGcXKAz5@qpbD( k%gY!#diFEAHBSKs08}%Vw6XDh$N&HU07*qoM6N<$f@Da>kN^Mx literal 0 HcmV?d00001 diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png b/src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..b3b291cafd762c98156d2276e8f76696506f6d56 GIT binary patch literal 2056 zcmaJ?dr%Yi8jTOlRNbkmq?H}dt?C$R|=bZ2Re&5;I z5=op8=kksV27|$gVg-rl7==E7GZuXxS2lf(4n9b<6iI?JkUUTaVImc9Is}N+U?!9Z zfr>4gFGFD%j3Y^vEJdW^bzC{Drhs+~#h})rYz!uBwLuHYvmgXWhcZLfDh{+fJF%olGEsPfX$}RnJhM&9tbdKbOx1%-s~Vclgnar86m*y zi-bngDKfZ;f|%E_&=rrQL=Y{PO3lm5qvQosU|lAa&f#$E8Vp7diU`tg(IB89NTVmu zDF`6FT&L0^Dp&*96~S~k2jP*>O#duFt$ib_(Z4<>^uVYFP)ns#X!erkfMW6ghpN?Y z(0U{h`YYdm6xJti(L&TjNDt@ekS!3OG+6@Cq*$b440G0&3)tNWdeZ3KW$}!DWWhXq=eHkeHBAKAj%Th>8vg5rpug zLTLhiD1%9x;|gGTjvCS+b6mw=+|YTsb}Oj0Xk-DTQ{_U67#*wzUX{#M&7TWvUcJ{` z#r(Oj=jBq-WT^JW{;kn-Tc~^N(>Jz77jKLYX;8Q8P+K3l$wWVjh1*1e$YjIq?t>d+ zJrg{82F1oSkyii-?0Or&P3k~iBQ2G9#x@kM|P0jwRc9Dpc=F&>Z4hPmsZwC?H zR*MgeB5tECWVw_A61LaEfbrDldk&x5I&pPm_`r0>kpr@co(b~{`i%KZ8U($sNUOK_ zCGvim`SJIfUp)pF_hpSN0=LsV+mPE%tjvE z{?$I&+frUq$=x;O&xrqJS{bSzjyXQw|jJ!+c@}XN`&B8!ikuT z(sM8v4;PpV|Fr!p%oq`;vRgK^k|=lCveu_)LLO) zd%1<+>6=2NNpW}|Uq z`RvCUPE<=Ew%0v zuk38pmu03F`QWn;{3*|UTTgQpBfsi_M2!(KJCHB z@4W!{CJ0t`?K#5Yzo^)W8*nH-vPmGDuCDvUzZeKCBJO(mwX~tVVs-_iyE*3S=;+ze zqm|(naoV9(hZ=rz9S_R6fHy)kqij!o_Cv_`dTi~!oTRZ;v*E^H@2~+?DtTpb*svF$ z@a-vlt}ZuaS23=(?(-$j@iCFUu4$ch-;q*7a0FXx5cbI}X%~O-J=;+B@g+*7x0Pn~ zJYGh?%}hT2{@Lj7WiL}IGlki7<9+A4h6N_VvX_Kq7nQa}j{eoP?JUs`VO(+%@hXYa z`Hq|GS+B0O#V>Ya8}FtPzWWecG-jgb+OF+nt)1TFlj{4V0Bp|PEAov$gufj7l&ydzOUH++(;Z}P?6F&;3&*EJ%@-w?yaG4lIeY3pN%@fZQNiO9P447<*ZRuH zL5kNY_d@T@9plqiI`}!~45wSC?pu3qC?MuB+ZWi|xFQ?(qv#PZVrfMGx{^TbFV61n z?b~1YhxWqT_j9|ibUj^PJAxmVr6~Ftw0A4w%q-`JR|6b^rz*)`x}DrVMss*?YX6c| z;p=v~JZ5I$omKkS)6gYihN`z#XB@h7?)_z*Hh1Er)%P&aE;Nhp>Wf?AFOrYX|$Ad(jb= zzsX6MIc1tPfo+7RgAVc)G}~8&_%dSh{cQ=0;S)Fb)-ZK{0rZ^Of%$pUnR`M? R%Om^0Sri>7Xo^bP@jq}SEWH2# literal 0 HcmV?d00001 From bc4bed6d3eeabd6e09db9dcb0be72145eddcfa31 Mon Sep 17 00:00:00 2001 From: VladimirE Date: Sun, 28 Jul 2013 17:54:10 +0300 Subject: [PATCH 010/541] CRM-238 --- src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index e7e1be2e9f0..dc7715aae5e 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -4294,7 +4294,9 @@ button.btn.minimize-button i.icon-minimize-active{ float: right; padding: 20px 0 0; width: 58%; - border: 1px solid #bbb; + img{ + border: 1px solid #bbb; + } } .map-item{ padding: 15px; From 3b3926f7512194f3e9c6bf62d73e41dd3b100e2d Mon Sep 17 00:00:00 2001 From: Vova Soroka Date: Mon, 29 Jul 2013 15:40:09 +0300 Subject: [PATCH 011/541] CRM-286: Correspondence visibility -CRM-294: Create doctrine entity classes in EmailBundle --- src/Oro/Bundle/EmailBundle/.gitignore | 2 + .../DataFixtures/ORM/LoadEmailOriginData.php | 24 + src/Oro/Bundle/EmailBundle/Entity/Email.php | 481 ++++++++++++++++++ .../EmailBundle/Entity/EmailAddress.php | 62 +++ .../EmailBundle/Entity/EmailAttachment.php | 118 +++++ .../Bundle/EmailBundle/Entity/EmailBody.php | 251 +++++++++ .../Bundle/EmailBundle/Entity/EmailFolder.php | 167 ++++++ .../Bundle/EmailBundle/Entity/EmailOrigin.php | 103 ++++ .../EmailBundle/Entity/EmailRecipient.php | 153 ++++++ .../EmailBundle/Entity/Util/EmailUtil.php | 38 ++ src/Oro/Bundle/EmailBundle/LICENSE | 19 + src/Oro/Bundle/EmailBundle/OroEmailBundle.php | 9 + .../Tests/Unit/Entity/EmailAddressTest.php | 31 ++ .../Tests/Unit/Entity/EmailAttachmentTest.php | 48 ++ .../Tests/Unit/Entity/EmailBodyTest.php | 89 ++++ .../Tests/Unit/Entity/EmailFolderTest.php | 62 +++ .../Tests/Unit/Entity/EmailOriginTest.php | 45 ++ .../Tests/Unit/Entity/EmailRecipientTest.php | 58 +++ .../Tests/Unit/Entity/EmailTest.php | 177 +++++++ .../Tests/Unit/Entity/Util/EmailUtilTest.php | 26 + .../Bundle/EmailBundle/Tests/bootstrap.php | 14 + src/Oro/Bundle/EmailBundle/composer.json | 23 + 22 files changed, 2000 insertions(+) create mode 100644 src/Oro/Bundle/EmailBundle/.gitignore create mode 100644 src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/Email.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailBody.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailOrigin.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php create mode 100644 src/Oro/Bundle/EmailBundle/LICENSE create mode 100644 src/Oro/Bundle/EmailBundle/OroEmailBundle.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAddressTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/bootstrap.php create mode 100644 src/Oro/Bundle/EmailBundle/composer.json diff --git a/src/Oro/Bundle/EmailBundle/.gitignore b/src/Oro/Bundle/EmailBundle/.gitignore new file mode 100644 index 00000000000..00ae1784e18 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/.gitignore @@ -0,0 +1,2 @@ +/PoC/* +*~ diff --git a/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php new file mode 100644 index 00000000000..fc4071207ef --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php @@ -0,0 +1,24 @@ +setName('BAP'); + + $manager->persist($emailOrigin); + $manager->flush(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Email.php b/src/Oro/Bundle/EmailBundle/Entity/Email.php new file mode 100644 index 00000000000..5774f43750c --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Email.php @@ -0,0 +1,481 @@ +recipients = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get entity created date/time + * + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->created; + } + + /** + * Get email subject + * + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Set email subject + * + * @param string $subject + * @return $this + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Get FROM email name + * + * @return string + */ + public function getFromName() + { + return $this->fromName; + } + + /** + * Set FROM email name + * + * @param string $fromName + * @return $this + */ + public function setFromName($fromName) + { + $this->fromName = $fromName; + + return $this; + } + + /** + * Get FROM email address + * + * @return EmailAddress + */ + public function getFromEmailAddress() + { + return $this->fromEmailAddress; + } + + /** + * Set FROM email address + * + * @param EmailAddress $fromEmailAddress + * @return $this + */ + public function setFromEmailAddress(EmailAddress $fromEmailAddress) + { + $this->fromEmailAddress = $fromEmailAddress; + + return $this; + } + + /** + * Get email recipients + * + * @param null|string $recipientType null to get all recipients, or 'to', 'cc' or 'bcc' if you need specific type of recipients + * @return EmailRecipient[] + */ + public function getRecipients($recipientType = null) + { + if ($recipientType === null) { + return $this->recipients; + } + + return $this->recipients->filter( + function ($recipient) use ($recipientType) { + /** @var EmailRecipient $recipient */ + return $recipient->getType() === $recipientType; + } + ); + } + + /** + * Add folder + * + * @param EmailRecipient $recipient + * @return $this + */ + public function addRecipient(EmailRecipient $recipient) + { + $this->recipients[] = $recipient; + + $recipient->setEmail($this); + + return $this; + } + + /** + * Get date/time when email received + * + * @return \DateTime + */ + public function getReceivedAt() + { + return $this->received; + } + + /** + * Set date/time when email received + * + * @param \DateTime $received + * @return $this + */ + public function setReceivedAt($received) + { + $this->received = $received; + + return $this; + } + + /** + * Get date/time when email sent + * + * @return \DateTime + */ + public function getSentAt() + { + return $this->sent; + } + + /** + * Set date/time when email sent + * + * @param \DateTime $sent + * @return $this + */ + public function setSentAt($sent) + { + $this->sent = $sent; + + return $this; + } + + /** + * Get email importance + * + * @return integer Can be one of *_IMPORTANCE constants + */ + public function getImportance() + { + return $this->importance; + } + + /** + * Set email importance + * + * @param integer $importance Can be one of *_IMPORTANCE constants + * @return $this + */ + public function setImportance($importance) + { + $this->importance = $importance; + + return $this; + } + + /** + * Get email internal date receives from an email server + * + * @return \DateTime + */ + public function getInternalDate() + { + return $this->internalDate; + } + + /** + * Set email internal date receives from an email server + * + * @param \DateTime $internalDate + * @return $this + */ + public function setInternalDate($internalDate) + { + $this->internalDate = $internalDate; + + return $this; + } + + /** + * Get value of email Message-ID header + * + * @return string + */ + public function getMessageId() + { + return $this->messageId; + } + + /** + * Set value of email Message-ID header + * + * @param string $messageId + * @return $this + */ + public function setMessageId($messageId) + { + $this->messageId = $messageId; + + return $this; + } + + /** + * Get email message id uses for group related messages + * + * @return string + */ + public function getXMessageId() + { + return $this->xMessageId; + } + + /** + * Set email message id uses for group related messages + * + * @param string $xMessageId + * @return $this + */ + public function setXMessageId($xMessageId) + { + $this->xMessageId = $xMessageId; + + return $this; + } + + /** + * Get email thread id uses for group related messages + * + * @return string + */ + public function getXThreadId() + { + return $this->xThreadId; + } + + /** + * Set email thread id uses for group related messages + * + * @param string $xThreadId + * @return $this + */ + public function setXThreadId($xThreadId) + { + $this->xThreadId = $xThreadId; + + return $this; + } + + /** + * Get email folder + * + * @return EmailFolder + */ + public function getFolder() + { + return $this->folder; + } + + /** + * Set email folder + * + * @param EmailFolder $folder + * @return $this + */ + public function setFolder(EmailFolder $folder) + { + $this->folder = $folder; + + return $this; + } + + /** + * Get cached email body + * + * @return EmailBody + */ + public function getEmailBody() + { + return $this->emailBody; + } + + /** + * Set email body + * + * @param EmailBody $emailBody + * @return $this + */ + public function setEmailBody(EmailBody $emailBody) + { + $this->emailBody = $emailBody; + + return $this; + } + + /** + * Pre persist event listener + * + * @ORM\PrePersist + */ + public function beforeSave() + { + if (!isset($this->importance)) { + $this->importance = self::NORMAL_IMPORTANCE; + } + $this->created = new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php new file mode 100644 index 00000000000..a49e0e0b4e3 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php @@ -0,0 +1,62 @@ +id; + } + + /** + * Get a 'pure' email address. + * It means that if the full email is "John Smith" the email address is john@example.com + * + * @return string + */ + public function getEmailAddress() + { + return $this->emailAddress; + } + + /** + * Set a 'pure' email address. + * It means that if the full email is "John Smith" the email address is john@example.com + * + * @param string $emailAddress + */ + public function setEmailAddress($emailAddress) + { + $this->emailAddress = $emailAddress; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php new file mode 100644 index 00000000000..aec8136e2aa --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php @@ -0,0 +1,118 @@ +id; + } + + /** + * Get attachment file name + * + * @return string + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * Set attachment file name + * + * @param string $fileName + */ + public function setFileName($fileName) + { + $this->fileName = $fileName; + } + + /** + * Get content type. It may be any MIME type + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Set content type + * + * @param string $contentType any MIME type + */ + public function setContentType($contentType) + { + $this->contentType = $contentType; + } + + /** + * Get email body + * + * @return EmailBody + */ + public function getEmailBody() + { + return $this->emailBody; + } + + /** + * Set email body + * + * @param EmailBody $emailBody + * @return $this + */ + public function setEmailBody(EmailBody $emailBody) + { + $this->emailBody = $emailBody; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php new file mode 100644 index 00000000000..0f182ab307c --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php @@ -0,0 +1,251 @@ +attachments = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get entity created date/time + * + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->created; + } + + /** + * Get body content. + * + * @return string + */ + public function getContent() + { + return $this->bodyContent; + } + + /** + * Set body content. + * + * @param string $bodyContent + */ + public function setContent($bodyContent) + { + $this->bodyContent = $bodyContent; + } + + /** + * Indicate whether email body is a text or html. + * + * @return bool true if body is text/plain; otherwise, the body content is text/html + */ + public function getBodyIsText() + { + return $this->bodyIsText; + } + + /** + * Set body content type. + * + * @param bool $bodyIsText true for text/plain, false for text/html + */ + public function setBodyIsText($bodyIsText) + { + $this->bodyIsText = $bodyIsText; + } + + /** + * Indicate whether email has attachments or not. + * + * @return bool true if body is text/plain; otherwise, the body content is text/html + */ + public function getHasAttachments() + { + return $this->hasAttachments; + } + + /** + * Set flag indicates whether there are attachments or not. + * + * @param bool $hasAttachments + */ + public function setHasAttachments($hasAttachments) + { + $this->hasAttachments = $hasAttachments; + } + + /** + * Indicate whether email is persistent or not. + * + * @return bool true if this email newer removed from the cache; otherwise, false + */ + public function getPersistent() + { + return $this->persistent; + } + + /** + * Set flag indicates whether email can be removed from the cache or not. + * + * @param bool $persistent true if this email newer removed from the cache; otherwise, false + */ + public function setPersistent($persistent) + { + $this->persistent = $persistent; + } + + /** + * Get email header + * + * @return Email + */ + public function getHeader() + { + return $this->header; + } + + /** + * Set email header + * + * @param Email $header + * @return $this + */ + public function setHeader(Email $header) + { + $this->header = $header; + + return $this; + } + + /** + * Get email attachments + * + * @return EmailAttachment[] + */ + public function getAttachments() + { + return $this->attachments; + } + + /** + * Add email attachment + * + * @param EmailAttachment $attachment + * @return $this + */ + public function addAttachment(EmailAttachment $attachment) + { + $this->setHasAttachments(true); + + $this->attachments[] = $attachment; + + $attachment->setEmailBody($this); + + return $this; + } + + /** + * Pre persist event listener + * + * @ORM\PrePersist + */ + public function beforeSave() + { + if (!isset($this->bodyIsText)) { + $this->bodyIsText = false; + } + if (!isset($this->hasAttachments)) { + $this->hasAttachments = false; + } + if (!isset($this->persistent)) { + $this->persistent = false; + } + $this->created = new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php new file mode 100644 index 00000000000..efe28e78c69 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php @@ -0,0 +1,167 @@ +emails = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get folder name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set folder name + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get folder type. + * + * @return string Can be 'inbox', 'sent', 'trash', 'drafts' or 'other' + */ + public function getType() + { + return $this->type; + } + + /** + * Set folder type + * + * @param string $type Can be 'inbox', 'sent', 'trash', 'drafts' or 'other' + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Get email folder origin + * + * @return EmailOrigin + */ + public function getOrigin() + { + return $this->origin; + } + + /** + * Set email folder origin + * + * @param EmailOrigin $origin + * @return $this + */ + public function setOrigin(EmailOrigin $origin) + { + $this->origin = $origin; + + return $this; + } + + /** + * Get emails + * + * @return Email[] + */ + public function getEmails() + { + return $this->emails; + } + + /** + * Add email + * + * @param Email $email + * @return $this + */ + public function addEmail(Email $email) + { + $this->emails[] = $email; + + $email->setFolder($this); + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailOrigin.php b/src/Oro/Bundle/EmailBundle/Entity/EmailOrigin.php new file mode 100644 index 00000000000..0b593c9b461 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailOrigin.php @@ -0,0 +1,103 @@ +folders = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get email origin name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set email origin name + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get email folders + * + * @return EmailFolder[] + */ + public function getFolders() + { + return $this->folders; + } + + /** + * Add folder + * + * @param EmailFolder $folder + * @return $this + */ + public function addFolder(EmailFolder $folder) + { + $this->folders[] = $folder; + + $folder->setOrigin($this); + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php b/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php new file mode 100644 index 00000000000..38cffc16571 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php @@ -0,0 +1,153 @@ +id; + } + + /** + * Get full email name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set full email name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get recipient type. + * + * @return string Can be 'to', 'cc' or 'bcc' + */ + public function getType() + { + return $this->type; + } + + /** + * Set recipient type + * + * @param string $type Can be 'to', 'cc' or 'bcc' + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Get email address + * + * @return EmailAddress + */ + public function getEmailAddress() + { + return $this->emailAddress; + } + + /** + * Set email address + * + * @param EmailAddress $emailAddress + * @return $this + */ + public function setEmailAddress(EmailAddress $emailAddress) + { + $this->emailAddress = $emailAddress; + + return $this; + } + + /** + * Get email + * + * @return Email + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set email + * + * @param Email $email + * @return $this + */ + public function setEmail(Email $email) + { + $this->email = $email; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php b/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php new file mode 100644 index 00000000000..c58d36f00b6 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php @@ -0,0 +1,38 @@ +; 'pure' email address john@example.com + * email address: John Smith ; 'pure' email address john@example.com + * email address: ; 'pure' email address john@example.com + * email address: john@example.com; 'pure' email address john@example.com + * + * @param string $fullEmailAddress + * @return string + */ + public static function extractPureEmailAddress($fullEmailAddress) + { + $atPos = strpos($fullEmailAddress, '@'); + if ($atPos === false) { + return $fullEmailAddress; + } + + $startPos = strrpos($fullEmailAddress, '<', -$atPos); + if ($startPos === false) { + return $fullEmailAddress; + } + + $endPos = strpos($fullEmailAddress, '>', $atPos); + if ($endPos === false) { + return $fullEmailAddress; + } + + return substr($fullEmailAddress, $startPos + 1, $endPos - $startPos - 1); + } +} diff --git a/src/Oro/Bundle/EmailBundle/LICENSE b/src/Oro/Bundle/EmailBundle/LICENSE new file mode 100644 index 00000000000..938870a7a30 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Oro, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Oro/Bundle/EmailBundle/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php new file mode 100644 index 00000000000..768ad2f41d5 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php @@ -0,0 +1,9 @@ +assertEquals(1, $entity->getId()); + } + + public function testEmailAddressGetterAndSetter() + { + $entity = new EmailAddress(); + $entity->setEmailAddress('test'); + $this->assertEquals('test', $entity->getEmailAddress()); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php new file mode 100644 index 00000000000..eb3810a74ff --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php @@ -0,0 +1,48 @@ +assertEquals(1, $entity->getId()); + } + + public function testFileNameGetterAndSetter() + { + $entity = new EmailAttachment(); + $entity->setFileName('test'); + $this->assertEquals('test', $entity->getFileName()); + } + + public function testContentTypeGetterAndSetter() + { + $entity = new EmailAttachment(); + $entity->setContentType('test'); + $this->assertEquals('test', $entity->getContentType()); + } + + public function testEmailBodyGetterAndSetter() + { + $emailBody = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailBody'); + + $entity = new EmailAttachment(); + $entity->setEmailBody($emailBody); + + $this->assertTrue($emailBody === $entity->getEmailBody()); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php new file mode 100644 index 00000000000..7278bf75125 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php @@ -0,0 +1,89 @@ +assertEquals(1, $entity->getId()); + } + + public function testContentGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setContent('test'); + $this->assertEquals('test', $entity->getContent()); + } + + public function testBodyIsTextGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setBodyIsText(true); + $this->assertEquals(true, $entity->getBodyIsText()); + } + + public function testHasAttachmentsGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setHasAttachments(true); + $this->assertEquals(true, $entity->getHasAttachments()); + } + + public function testPersistentGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setPersistent(true); + $this->assertEquals(true, $entity->getPersistent()); + } + + public function testHeaderGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailBody(); + $entity->setHeader($email); + + $this->assertTrue($email === $entity->getHeader()); + } + + public function testAttachmentGetterAndSetter() + { + $attachment = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAttachment'); + + $entity = new EmailBody(); + $entity->addAttachment($attachment); + + $attachments = $entity->getAttachments(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $attachments); + $this->assertCount(1, $attachments); + $this->assertTrue($attachment === $attachments[0]); + } + + public function testBeforeSave() + { + $entity = new EmailBody(); + $entity->beforeSave(); + + $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + + $this->assertEquals(false, $entity->getBodyIsText()); + $this->assertEquals(false, $entity->getHasAttachments()); + $this->assertEquals(false, $entity->getPersistent()); + $this->assertGreaterThanOrEqual($createdAt, $entity->getCreatedAt()); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php new file mode 100644 index 00000000000..9331f25ca10 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php @@ -0,0 +1,62 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailFolder(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testTypeGetterAndSetter() + { + $entity = new EmailFolder(); + $entity->setType('test'); + $this->assertEquals('test', $entity->getType()); + } + + public function testOriginGetterAndSetter() + { + $origin = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOrigin'); + + $entity = new EmailFolder(); + $entity->setOrigin($origin); + + $this->assertTrue($origin === $entity->getOrigin()); + } + + public function testEmailGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailFolder(); + $entity->addEmail($email); + + $emails = $entity->getEmails(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $emails); + $this->assertCount(1, $emails); + $this->assertTrue($email === $emails[0]); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php new file mode 100644 index 00000000000..3a25264967f --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php @@ -0,0 +1,45 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailOrigin(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testFolderGetterAndSetter() + { + $folder = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailFolder'); + + $entity = new EmailOrigin(); + $entity->addFolder($folder); + + $folders = $entity->getFolders(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $folders); + $this->assertCount(1, $folders); + $this->assertTrue($folder === $folders[0]); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php new file mode 100644 index 00000000000..d4ed8fcea1d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php @@ -0,0 +1,58 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailRecipient(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testTypeGetterAndSetter() + { + $entity = new EmailRecipient(); + $entity->setType('test'); + $this->assertEquals('test', $entity->getType()); + } + + public function testEmailAddressGetterAndSetter() + { + $emailAddress = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAddress'); + + $entity = new EmailRecipient(); + $entity->setEmailAddress($emailAddress); + + $this->assertTrue($emailAddress === $entity->getEmailAddress()); + } + + public function testEmailGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailRecipient(); + $entity->setEmail($email); + + $this->assertTrue($email === $entity->getEmail()); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php new file mode 100644 index 00000000000..5659487de0a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php @@ -0,0 +1,177 @@ +assertEquals(1, $entity->getId()); + } + + public function testSubjectGetterAndSetter() + { + $entity = new Email(); + $entity->setSubject('test'); + $this->assertEquals('test', $entity->getSubject()); + } + + public function testFromNameGetterAndSetter() + { + $entity = new Email(); + $entity->setFromName('test'); + $this->assertEquals('test', $entity->getFromName()); + } + + public function testFromEmailAddressGetterAndSetter() + { + $emailAddress = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAddress'); + + $entity = new Email(); + $entity->setFromEmailAddress($emailAddress); + + $this->assertTrue($emailAddress === $entity->getFromEmailAddress()); + } + + public function testRecipientGetterAndSetter() + { + $toRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $toRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('to')); + + $ccRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $ccRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('cc')); + + $bccRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $bccRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('bcc')); + + $entity = new Email(); + $entity->addRecipient($toRecipient); + $entity->addRecipient($ccRecipient); + $entity->addRecipient($bccRecipient); + + $recipients = $entity->getRecipients(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(3, $recipients); + $this->assertTrue($toRecipient === $recipients[0]); + $this->assertTrue($ccRecipient === $recipients[1]); + $this->assertTrue($bccRecipient === $recipients[2]); + + $recipients = $entity->getRecipients('to'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($toRecipient === $recipients->first()); + + $recipients = $entity->getRecipients('cc'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($ccRecipient === $recipients->first()); + + $recipients = $entity->getRecipients('bcc'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($bccRecipient === $recipients->first()); + } + + public function testReceivedAtGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setReceivedAt($date); + $this->assertEquals($date, $entity->getReceivedAt()); + } + + public function testSentAtGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setSentAt($date); + $this->assertEquals($date, $entity->getSentAt()); + } + + public function testImportanceGetterAndSetter() + { + $entity = new Email(); + $entity->setImportance(1); + $this->assertEquals(1, $entity->getImportance()); + } + + public function testInternalDateGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setInternalDate($date); + $this->assertEquals($date, $entity->getInternalDate()); + } + + public function testMessageIdGetterAndSetter() + { + $entity = new Email(); + $entity->setMessageId('test'); + $this->assertEquals('test', $entity->getMessageId()); + } + + public function testXMessageIdGetterAndSetter() + { + $entity = new Email(); + $entity->setXMessageId('test'); + $this->assertEquals('test', $entity->getXMessageId()); + } + + public function testXThreadIdGetterAndSetter() + { + $entity = new Email(); + $entity->setXThreadId('test'); + $this->assertEquals('test', $entity->getXThreadId()); + } + + public function testFolderGetterAndSetter() + { + $folder = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailFolder'); + + $entity = new Email(); + $entity->setFolder($folder); + + $this->assertTrue($folder === $entity->getFolder()); + } + + public function testEmailBodyGetterAndSetter() + { + $emailBody = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailBody'); + + $entity = new Email(); + $entity->setEmailBody($emailBody); + + $this->assertTrue($emailBody === $entity->getEmailBody()); + } + + public function testBeforeSave() + { + $entity = new Email(); + $entity->beforeSave(); + + $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + + $this->assertEquals(Email::NORMAL_IMPORTANCE, $entity->getImportance()); + $this->assertGreaterThanOrEqual($createdAt, $entity->getCreatedAt()); + } + + private static function setId($obj, $val) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php new file mode 100644 index 00000000000..ba31e56c938 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php @@ -0,0 +1,26 @@ +assertEquals($pureEmailAddress, EmailUtil::extractPureEmailAddress($fullEmailAddress)); + } + + public static function extractPureEmailAddressProvider() + { + return array( + array('john@example.com', 'john@example.com'), + array('', 'john@example.com'), + array('John Smith ', 'john@example.com'), + array('"John Smith" ', 'john@example.com'), + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php b/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php new file mode 100644 index 00000000000..697a4bfd271 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php @@ -0,0 +1,14 @@ +=5.3.3", + "symfony/symfony": "2.1.*", + "oro/user-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\EmailBundle": "" } + }, + "target-dir": "Oro/Bundle/EmailBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} From de73d45c3ea255e855f89d69fd8a5baedd669dca Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Mon, 29 Jul 2013 16:42:54 +0200 Subject: [PATCH 012/541] BAP-134 : add sot on option default value when there is no option value --- .../FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php index 1804dc54ec6..a55f186411c 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php @@ -342,6 +342,7 @@ public function addAttributeOrderBy(AbstractAttribute $attribute, $direction) $condition = $joinAliasOptVal.'.locale = '.$this->qb->expr()->literal($this->getLocale()); $this->qb->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); + $this->qb->addOrderBy($joinAliasOpt.'.defaultValue', $direction); $this->qb->addOrderBy($joinAliasOptVal.'.value', $direction); } else { From 67d6e4ab5588568879b59cec34d5c93d91112ef5 Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 17:43:34 +0300 Subject: [PATCH 013/541] BAP-1247: Implement query parser - added configuration validation --- .../Datagrid/QueryConverter/YamlConverter.php | 9 +- .../ReportConfiguration.php | 115 ++++++++++++++++++ .../QueryConverter/YamlConverterTest.php | 114 ++++++++++++++--- 3 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php diff --git a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php index 0a235646a2c..c3129d4f36c 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php @@ -3,11 +3,13 @@ namespace Oro\Bundle\GridBundle\Datagrid\QueryConverter; use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Config\Definition\Processor; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\EntityManager; use Oro\Bundle\GridBundle\Datagrid\QueryConverterInterface; +use Oro\Bundle\GridBundle\DependencyInjection\ReportConfiguration; class YamlConverter implements QueryConverterInterface { @@ -20,7 +22,10 @@ public function parse($value, EntityManager $em) $value = Yaml::parse($value); } - $qb = $em->createQueryBuilder(); + $processor = new Processor(); + + $value = $processor->processConfiguration(new ReportConfiguration(), array('report' => $value)); + $qb = $em->createQueryBuilder(); if (!isset($value['from'])) { throw new \RuntimeException('Missing mandatory "from" section'); @@ -90,6 +95,6 @@ public function parse($value, EntityManager $em) */ public function dump(QueryBuilder $input) { - ; + return ''; } } \ No newline at end of file diff --git a/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php b/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php new file mode 100644 index 00000000000..4ab8fc441c6 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php @@ -0,0 +1,115 @@ +root('report') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->booleanNode('distinct') + ->defaultFalse() + ->end() + ->scalarNode('select') + ->defaultValue('*') + ->end() + ->arrayNode('from') + ->requiresAtLeastOneElement() + ->prototype('array') + ->children() + ->scalarNode('table') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('alias')->end() + ->end() + ->end() + ->end() + ->arrayNode('join') + ->append($this->addJoinNode('left')) + ->append($this->addJoinNode('inner')) + ->end() + ->arrayNode('where') + ->append($this->addWhereNode('and')) + ->append($this->addWhereNode('or')) + ->end() + ->scalarNode('groupBy')->end() + ->scalarNode('having')->end() + ->arrayNode('orderBy') + ->prototype('array') + ->children() + ->scalarNode('column') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('dir') + ->defaultValue('asc') + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $builder; + } + + /** + * + * @param string $name Join type ('left', 'inner') + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + protected function addJoinNode($name) + { + if (!in_array($name, array('left', 'inner'))) { + throw new InvalidConfigurationException(sprintf('Invalid join type "%s"', $name)); + } + + $builder = new TreeBuilder(); + + return $builder->root($name) + ->requiresAtLeastOneElement() + ->prototype('array') + ->children() + ->scalarNode('join') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('alias')->end() + ->end() + ->end(); + } + + /** + * + * @param string $name Where type ('and', 'or') + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + protected function addWhereNode($name) + { + if (!in_array($name, array('and', 'or'))) { + throw new InvalidConfigurationException(sprintf('Invalid where type "%s"', $name)); + } + + $builder = new TreeBuilder(); + + return $builder->root($name) + ->requiresAtLeastOneElement() + ->prototype('scalar') + ->end(); + } +} diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php index 4f43d153e13..f5059f08fc8 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php @@ -1,39 +1,115 @@ converter = new YamlConverter(); + $this->em = $this->getMock('Doctrine\ORM\EntityManager', array(), array(), '', false); + + $this->em + ->expects($this->any()) + ->method('createQueryBuilder') + ->withAnyParameters() + ->will($this->returnValue(new QueryBuilder($this->em))); + + } protected function tearDown() { - unset($this->model); + unset($this->converter, $this->em); } - public function testGetQueryBuilder() + /** + * @expectedException \RuntimeException + */ + public function testParseException() + { + $this->converter->parse(array(), $this->em); + } + + public function testParse() { - $queryBuilderMock = $this->getMock('Doctrine\ORM\QueryBuilder', array(), array(), '', false); - $this->model = new ProxyQuery($queryBuilderMock); - $this->assertEquals($queryBuilderMock, $this->model->getQueryBuilder()); + $value = array( + 'from' => array( + array( + 'table' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'alias' => 'u', + ) + ), + 'select' => 'u', + 'distinct' => true, + 'join' => array( + 'inner' => array( + 'join' => 'u.articles', + 'alias' => 'a' + ), + 'left' => array( + 'join' => 'email', + 'alias' => 'e' + ) + ), + 'groupBy' => 'u.id', + 'having' => 'COUNT(u.id) > 0', + 'where' => array( + 'and' => array( + 'u.status IS NULL' + ), + 'or' => array( + 'u.id < 100' + ), + ), + 'orderBy' => array( + array( + 'column' => 'u.id', + 'dir' => 'desc', + ), + ), + ); + + $qb = $this->converter->parse($value, $this->em); + + $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $qb); + $this->assertNotEmpty($qb->getDQLPart('select')); + $this->assertNotEmpty($qb->getDQLPart('from')); + $this->assertNotEmpty($qb->getDQLPart('orderBy')); + $this->assertTrue($qb->getDQLPart('distinct')); + + $value = ' +select: "u" +from: + - { table: Doctrine\Tests\Models\CMS\CmsUser, alias: u }'; + + $qb = $this->converter->parse($value, $this->em); + + $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $qb); } - public function testSetParameter() + public function testDump() { - $testName = 'test_name'; - $testValue = 'test_value'; + $qb = new QueryBuilder($this->em); - $queryBuilderMock = $this->getMock('Doctrine\ORM\QueryBuilder', array('setParameter'), array(), '', false); - $queryBuilderMock->expects($this->once()) - ->method('setParameter') - ->with($testName, $testValue); + $result = $this->converter->dump($qb); - $this->model = new ProxyQuery($queryBuilderMock); - $this->model->setParameter($testName, $testValue); + $this->assertInternalType('string', $result); } } From ec9fe7e839788fbf49e4ac46d5ebb927840f54e9 Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 18:30:49 +0300 Subject: [PATCH 014/541] BAP-1247: Implement query parser - added documentation --- .../doc/reference/backend/reports.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md new file mode 100644 index 00000000000..50cf49004f6 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -0,0 +1,64 @@ +Events +------ + +Datagrid events are designed to inject external listeners logic into datagrid functionality. +Datagrid events are implemented based on Symfony 2 events using event dispatcher and kernel event listener. + +#### Class Description + +* **EventDispatcher \ DatagridEventInterface** - basic interface for all datagrid events; +* **EventDispatcher \ AbstractDatagridEvent** - implements datagrid event interface +and extends standard Symfony 2 event; +* **EventDispatcher \ ResultDatagridEven**t (event name: _oro\_grid.datagrid.result_) - allows to +external result listeners to set and get datagrid result rows. + +#### Example of usage + +**Result Listener** + +``` php +namespace Oro\Bundle\SearchBundle\Datagrid; + +use Oro\Bundle\GridBundle\EventDispatcher\ResultDatagridEvent; + +class EntityResultListener +{ + /** + * @var string + */ + protected $datagridName; + + /** + * @param string $datagridName + */ + public function __construct($datagridName) + { + $this->datagridName = $datagridName; + } + + /** + * @param ResultDatagridEvent $event + */ + public function processResult(ResultDatagridEvent $event) + { + if (!$event->isDatagridName($this->datagridName)) { + return; + } + + // main processing logic... + } +} +``` + +**Configuration** + +``` +oro_search.datagrid_results.entity_result_listener: + class: Oro\Bundle\SearchBundle\Datagrid\EntityResultListener + arguments: + - oro_search_results + tags: + - name: kernel.event_listener + event: oro_grid.datagrid.result + method: processResult +``` From 3091cc472cd0f685f3ce5d01cc8403afa999f873 Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 18:31:11 +0300 Subject: [PATCH 015/541] BAP-1247: Implement query parser - added documentation --- .../Bundle/GridBundle/Resources/doc/index.md | 1 + .../doc/reference/backend/reports.md | 187 ++++++++++++++---- 2 files changed, 152 insertions(+), 36 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/index.md b/src/Oro/Bundle/GridBundle/Resources/doc/index.md index c11e32a1df0..6ad2ff15387 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/index.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/index.md @@ -23,6 +23,7 @@ OroGridBundle Documentation - [Grid Rendering](./reference/backend/grid-rendering.md) - [Events](./reference/backend/events.md) - [Translations](./reference/backend/translations.md) + - [Reports](./reference/backend/reports.md) - **Frontend Architecture** - [Overview](./reference/frontend/overview.md) - [Backbone Developer Introduction](./reference/frontend/backbone-developer-introduction.md) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md index 50cf49004f6..fd585a6753a 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -1,64 +1,179 @@ -Events ------- +Reports +------- -Datagrid events are designed to inject external listeners logic into datagrid functionality. -Datagrid events are implemented based on Symfony 2 events using event dispatcher and kernel event listener. +Datagrid bundle provides basic functionality to build reports based on defined structure. PHP array or YAML string +supported as a storage. -#### Class Description +#### Structure definition + +*name* - report name, reserved for future use +*distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) +*select* - string representing column list to select +*from* - array of table/alias declarations. Each element is an array with the following keys: + *table* - entity declaration, for example "OroUserBundle:User" + *alias* - [optional] alias for the entity/table name +*join* - array of join declarations. May contain two children: + *inner* - adds inner join. Format: + *join* - entity relation name, for example "u.articles" + *alias* - [optional] alias for a relation + *left* - adds left join. Format: + *join* - entity relation name, for example "u.articles" + *alias* - [optional] alias for a relation +*where* - array of where declarations. May contain two children: + *and* - adds "AND" where clause. You can use multiple declarations as array elements. + *or* - adds "OR" where clause. You can use multiple declarations as array elements. +*groupBy* - group definition as a string +*having* - "HAVING" clause definition as a string +*orderBy* - array of order declarations. Format: + *column* - order column + *dir* - [optional] sort direction, "asc" by default -* **EventDispatcher \ DatagridEventInterface** - basic interface for all datagrid events; -* **EventDispatcher \ AbstractDatagridEvent** - implements datagrid event interface -and extends standard Symfony 2 event; -* **EventDispatcher \ ResultDatagridEven**t (event name: _oro\_grid.datagrid.result_) - allows to -external result listeners to set and get datagrid result rows. #### Example of usage -**Result Listener** +**YAML definition** + +``` yaml +reports: + - + name: DemoReport + distinct: false + select: "u.username, u.firstName, u.loginCount, a.apiKey, COUNT(u.firstName) AS cnt" + from: + - { table: OroUserBundle:User, alias: u } + join: + left: + - { join: u.api, alias: a } + - { join: u.statuses, alias: s } + where: + and: + - "u.loginCount >= 0" + or: + - "u.enabled = 0" + groupBy: "u.firstName" + having: "COUNT(u.firstName) > 1" + orderBy: + - { column: "u.firstName", dir: "asc" } +``` + +``` php + +``` + +**Configuration** + +services.yml +``` yaml +parameters: + acme_demo_grid.report_grid.manager.class: Acme\Bundle\DemoGridBundle\Datagrid\ReportDatagridManager + +services: +acme_demo_grid.report_grid.manager: + class: %acme_demo_grid.report_grid.manager.class% + tags: + - name: oro_grid.datagrid.manager + datagrid_name: report + entity_hint: reports + route_name: acme_demo_gridbundle_report_list +``` + +**Datagrid declaration** ``` php -namespace Oro\Bundle\SearchBundle\Datagrid; +datagridName = $datagridName; + // ... } /** - * @param ResultDatagridEvent $event + * {@inheritdoc} */ - public function processResult(ResultDatagridEvent $event) + protected function createQuery() { - if (!$event->isDatagridName($this->datagridName)) { - return; - } + $input = Yaml::parse(file_get_contents(__DIR__ . '/../Resources/config/reports.yml')); + $converter = new YamlConverter(); + + $this->queryFactory->setQueryBuilder( + $converter->parse($input['reports'][0], $this->entityManager) + ); - // main processing logic... + return $this->queryFactory->createQuery(); + } + + /** + * @param EntityManager $entityManager + */ + public function setEntityManager(EntityManager $entityManager) + { + $this->entityManager = $entityManager; } } ``` -**Configuration** +**Controller** -``` -oro_search.datagrid_results.entity_result_listener: - class: Oro\Bundle\SearchBundle\Datagrid\EntityResultListener - arguments: - - oro_search_results - tags: - - name: kernel.event_listener - event: oro_grid.datagrid.result - method: processResult -``` +``` php +get('acme_demo_grid.report_grid.manager'); + + $gridManager->setEntityManager($this->getDoctrine()->getManager()); + + $grid = $gridManager->getDatagrid(); + $gridView = $grid->createView(); + + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($gridView); + } + + return array('datagrid' => $gridView); + } +} +``` \ No newline at end of file From 94bcf8700663c4b860c1be2a9fac9615d6d5d6f0 Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 18:59:30 +0300 Subject: [PATCH 016/541] BAP-1247: Implement query parser - tests updated --- .../Datagrid/QueryConverter/YamlConverterTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php index f5059f08fc8..e4288fe5ad4 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Datagrid/QueryConverter/YamlConverterTest.php @@ -50,6 +50,7 @@ public function testParseException() public function testParse() { $value = array( + 'name' => 'Test Report', 'from' => array( array( 'table' => 'Doctrine\Tests\Models\CMS\CmsUser', @@ -60,12 +61,16 @@ public function testParse() 'distinct' => true, 'join' => array( 'inner' => array( - 'join' => 'u.articles', - 'alias' => 'a' + array( + 'join' => 'u.articles', + 'alias' => 'a' + ), ), 'left' => array( - 'join' => 'email', - 'alias' => 'e' + array( + 'join' => 'email', + 'alias' => 'e' + ) ) ), 'groupBy' => 'u.id', @@ -95,6 +100,7 @@ public function testParse() $this->assertTrue($qb->getDQLPart('distinct')); $value = ' +name: "Test Report" select: "u" from: - { table: Doctrine\Tests\Models\CMS\CmsUser, alias: u }'; From b153f374f11402f5c1c3da88c3445cbf14b5356b Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 19:11:29 +0300 Subject: [PATCH 017/541] BAP-1247: Implement query parser - documentation syntax fix --- .../Resources/doc/reference/backend/reports.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md index fd585a6753a..d8485e455bd 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -6,25 +6,25 @@ supported as a storage. #### Structure definition -*name* - report name, reserved for future use -*distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) -*select* - string representing column list to select -*from* - array of table/alias declarations. Each element is an array with the following keys: +- *name* - report name, reserved for future use +- *distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) +- *select* - string representing column list to select +- *from* - array of table/alias declarations. Each element is an array with the following keys: *table* - entity declaration, for example "OroUserBundle:User" *alias* - [optional] alias for the entity/table name -*join* - array of join declarations. May contain two children: +- *join* - array of join declarations. May contain two children: *inner* - adds inner join. Format: *join* - entity relation name, for example "u.articles" *alias* - [optional] alias for a relation *left* - adds left join. Format: *join* - entity relation name, for example "u.articles" *alias* - [optional] alias for a relation -*where* - array of where declarations. May contain two children: +- *where* - array of where declarations. May contain two children: *and* - adds "AND" where clause. You can use multiple declarations as array elements. *or* - adds "OR" where clause. You can use multiple declarations as array elements. -*groupBy* - group definition as a string -*having* - "HAVING" clause definition as a string -*orderBy* - array of order declarations. Format: +- *groupBy* - group definition as a string +- *having* - "HAVING" clause definition as a string +- *orderBy* - array of order declarations. Format: *column* - order column *dir* - [optional] sort direction, "asc" by default From 71dee7516e9bf689ae24812d18dd628658a969ec Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 19:18:06 +0300 Subject: [PATCH 018/541] BAP-1247: Implement query parser - documentation syntax fix --- .../doc/reference/backend/reports.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md index d8485e455bd..4321cda1eee 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -6,27 +6,27 @@ supported as a storage. #### Structure definition -- *name* - report name, reserved for future use -- *distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) -- *select* - string representing column list to select -- *from* - array of table/alias declarations. Each element is an array with the following keys: - *table* - entity declaration, for example "OroUserBundle:User" - *alias* - [optional] alias for the entity/table name -- *join* - array of join declarations. May contain two children: +- *name* - report name, reserved for future use +- *distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) +- *select* - string representing column list to select +- *from* - array of table/alias declarations. Each element is an array with the following keys: + *table* - entity declaration, for example "OroUserBundle:User" + *alias* - [optional] alias for the entity/table name +- *join* - array of join declarations. May contain two children: *inner* - adds inner join. Format: *join* - entity relation name, for example "u.articles" *alias* - [optional] alias for a relation *left* - adds left join. Format: *join* - entity relation name, for example "u.articles" *alias* - [optional] alias for a relation -- *where* - array of where declarations. May contain two children: - *and* - adds "AND" where clause. You can use multiple declarations as array elements. - *or* - adds "OR" where clause. You can use multiple declarations as array elements. -- *groupBy* - group definition as a string -- *having* - "HAVING" clause definition as a string -- *orderBy* - array of order declarations. Format: - *column* - order column - *dir* - [optional] sort direction, "asc" by default +- *where* - array of where declarations. May contain two children: + *and* - adds "AND" where clause. You can use multiple declarations as array elements. + *or* - adds "OR" where clause. You can use multiple declarations as array elements. +- *groupBy* - group definition as a string +- *having* - "HAVING" clause definition as a string +- *orderBy* - array of order declarations. Format: + *column* - order column + *dir* - [optional] sort direction, "asc" by default #### Example of usage From 3327c8a511b2238a9a985b8e05d88bb6bee78fc3 Mon Sep 17 00:00:00 2001 From: Dan Yasnyuk Date: Mon, 29 Jul 2013 19:36:02 +0300 Subject: [PATCH 019/541] BAP-1247: Implement query parser - documentation syntax fix --- .../doc/reference/backend/reports.md | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md index 4321cda1eee..70f93d95b28 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -10,29 +10,28 @@ supported as a storage. - *distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) - *select* - string representing column list to select - *from* - array of table/alias declarations. Each element is an array with the following keys: - *table* - entity declaration, for example "OroUserBundle:User" - *alias* - [optional] alias for the entity/table name + - *table* - entity declaration, for example "OroUserBundle:User" + - *alias* - [optional] alias for the entity/table name - *join* - array of join declarations. May contain two children: - *inner* - adds inner join. Format: - *join* - entity relation name, for example "u.articles" - *alias* - [optional] alias for a relation - *left* - adds left join. Format: - *join* - entity relation name, for example "u.articles" - *alias* - [optional] alias for a relation + - *inner* - adds inner join. Format: + - *join* - entity relation name, for example "u.articles" + - *alias* - [optional] alias for a relation + - *left* - adds left join. Format: + - *join* - entity relation name, for example "u.articles" + - *alias* - [optional] alias for a relation - *where* - array of where declarations. May contain two children: - *and* - adds "AND" where clause. You can use multiple declarations as array elements. - *or* - adds "OR" where clause. You can use multiple declarations as array elements. + - *and* - adds "AND" where clause. You can use multiple declarations as array elements. + - *or* - adds "OR" where clause. You can use multiple declarations as array elements. - *groupBy* - group definition as a string - *having* - "HAVING" clause definition as a string - *orderBy* - array of order declarations. Format: - *column* - order column - *dir* - [optional] sort direction, "asc" by default + - *column* - order column + - *dir* - [optional] sort direction, "asc" by default #### Example of usage **YAML definition** - ``` yaml reports: - @@ -56,10 +55,6 @@ reports: - { column: "u.firstName", dir: "asc" } ``` -``` php - -``` - **Configuration** services.yml @@ -78,7 +73,6 @@ acme_demo_grid.report_grid.manager: ``` **Datagrid declaration** - ``` php Date: Mon, 29 Jul 2013 16:47:25 +0000 Subject: [PATCH 020/541] BAP-1286: Hide pager on empty grid - hide pager on grid if no results --- .../GridBundle/Resources/public/js/app/datagrid/grid.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js index c24133ec558..80dbca97a16 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js @@ -324,7 +324,9 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ * Renders grid toolbar. */ renderToolbar: function() { - this.$(this.selectors.toolbar).append(this.toolbar.render().$el); + if (this.body.rows.length > 0) { + this.$(this.selectors.toolbar).append(this.toolbar.render().$el); + } }, /** From af1796008bfff6f4c5e50bd0933474c208d14a00 Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Mon, 29 Jul 2013 17:12:00 +0000 Subject: [PATCH 021/541] BAP-1286: Hide pager on empty grid - hide fix --- .../GridBundle/Resources/public/js/app/datagrid/grid.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js index 80dbca97a16..e69adcc04bb 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js @@ -324,9 +324,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ * Renders grid toolbar. */ renderToolbar: function() { - if (this.body.rows.length > 0) { - this.$(this.selectors.toolbar).append(this.toolbar.render().$el); - } + this.$(this.selectors.toolbar).append(this.toolbar.render().$el); }, /** @@ -384,10 +382,12 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ */ _updateNoDataBlock: function() { if (this.collection.models.length > 0) { + this.$(this.selectors.toolbar).show(); this.$(this.selectors.grid).show(); this.$(this.selectors.noDataBlock).hide(); } else { this.$(this.selectors.grid).hide(); + this.$(this.selectors.toolbar).hide(); this.$(this.selectors.noDataBlock).show(); } }, From f94a3988d822634d447e8d1bb604e8a47d68a06d Mon Sep 17 00:00:00 2001 From: Michael Banin Date: Tue, 30 Jul 2013 17:48:34 +0300 Subject: [PATCH 022/541] BAP-1173 Fucntional Selenium Tests for Tags ACL --- .../TestFrameworkBundle/Pages/Objects/Tag.php | 7 +- .../Pages/Objects/Tags.php | 12 +- .../Bundle/TestFrameworkBundle/Pages/Page.php | 7 + .../Tests/Selenium/Tags/TagsAcl.php | 234 ++++++++++++++++++ 4 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php index 37b45e405dd..34adc1e81c2 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php @@ -14,10 +14,11 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); } - public function init() + public function init($new = true) { - $this->tagname = $this->byId('oro_tag_tag_form_name'); - + if ($new) { + $this->tagname = $this->byId('oro_tag_tag_form_name'); + } return $this; } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php index 3f9d4bde9ea..3ee14f123b9 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php @@ -14,7 +14,7 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); } - public function add() + public function add($new = true) { $this->test->byXPath("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]")->click(); //due to bug BAP-965 @@ -22,7 +22,7 @@ public function add() $this->waitPageToLoad(); $this->waitForAjax(); $tag = new Tag($this->test); - return $tag->init(); + return $tag->init($new); } public function open($entityData = array()) @@ -59,4 +59,12 @@ public function delete() return $this; } + + public function checkContextMenu($tagname, $contextname) + { + $this->filterBy('Tag', $tagname); + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + $this->assertElementNotPresent("//td[@class='action-cell']//a[@title= '{$contextname}']"); + } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php index 95bbc9faff0..89c8d4bb62a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\TestFrameworkBundle\Pages; +use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Login; use PHPUnit_Framework_Assert; use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Users; use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Roles; @@ -203,4 +204,10 @@ protected function clearInput($element) $tx = $element->value(); } } + + public function logout() + { + $this->url('/user/logout'); + return new Login($this->test); + } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php new file mode 100644 index 00000000000..60b0747485b --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php @@ -0,0 +1,234 @@ + 'NEW_LABEL_', 'ROLE_NAME' => 'NEW_ROLE_'); + + protected function setUp() + { + $this->setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + public function testCreateRole() + { + $randomPrefix = mt_rand(); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openRoles() + ->add() + ->setName('ROLE_NAME_' . $randomPrefix) + ->setLabel('Label_' . $randomPrefix) + ->selectAcl('Root') + ->save() + ->assertMessage('Role successfully saved') + ->close(); + + return ($randomPrefix); + } + + /** + * @depends testCreateRole + * @param $role + * @return string + */ + public function testCreateUser($role) + { + $username = 'User_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openUsers() + ->add() + ->assertTitle('Create User - Users - System') + ->setUsername($username) + ->enable() + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array('Label_' . $role)) + ->save() + ->assertMessage('User successfully saved') + ->close() + ->assertTitle('Users - System'); + + return $username; + } + + /** + * @depends testCreateUser + * @return string + */ + public function testCreateTag() + { + $tagname = 'Tag_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTags() + ->add() + ->assertTitle('Create Tag - Tags - System') + ->setTagname($tagname) + ->save() + ->assertMessage('Tag successfully saved') + ->assertTitle('Tags - System') + ->close(); + + return $tagname; + } + + /** + * @depends testCreateUser + * @depends testCreateRole + * @depends testCreateTag + * @param $username + * @param $role + * @param $tagname + * @param string $aclcase + * @dataProvider columnTitle + */ + public function testTagAcl($aclcase, $username, $role, $tagname) + { + $role = 'ROLE_NAME_' . $role; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit(); + switch ($aclcase) { + case 'delete': + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Delete tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Delete'); + break; + case 'update': + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Update tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Update'); + break; + case 'create': + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Create tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]"); + break; + case 'view list': + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('View list of tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertTitle('403 - Forbidden'); + break; + case 'unassign global': + $username = 'user' . mt_rand(); + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Tag unassign global') + ->save() + ->openUsers() + ->add() + ->setUsername($username) + ->enable() + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array('Manager')) + ->setTag($tagname) + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openUsers() + ->filterBy('Username', $username) + ->open(array($username)) + ->edit() + ->assertElementNotpresent + ("//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']"); + break; + case 'assign/unassign': + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Tag assign/unassign') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openAccounts() + ->add(false) + ->assertElementPresent("//div[@class='select2-container select2-container-multi select2-container-disabled']"); + break; + } + } + + /** + * Data provider for Tags ACL test + * + * @return array + */ + public function columnTitle() + { + return array( + 'delete' => array('delete'), + 'update' => array('update'), + 'create' => array('create'), + 'view list' => array('view list'), + 'unassign global' => array('unassign global'), + 'assign/unassign' => array('assign/unassign'), + ); + } +} From 8f84fb684a99a7a7f671e258156370bb05643b54 Mon Sep 17 00:00:00 2001 From: Michael Banin Date: Tue, 30 Jul 2013 17:54:04 +0300 Subject: [PATCH 023/541] BAP-1173 Functional Selenium test for Tags ACL - Fixed test --- .../Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php index 60b0747485b..d9548f6fd10 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php @@ -8,7 +8,6 @@ class TagsAcl extends \PHPUnit_Extensions_Selenium2TestCase { protected $coverageScriptUrl = PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL_COVERAGE; - protected $newRole = array('LABEL' => 'NEW_LABEL_', 'ROLE_NAME' => 'NEW_ROLE_'); protected function setUp() { @@ -196,7 +195,7 @@ public function testTagAcl($aclcase, $username, $role, $tagname) ->open(array($username)) ->edit() ->assertElementNotpresent - ("//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']"); + ("//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']"); break; case 'assign/unassign': $login->openRoles() From f6b3baf77d57448f5e53162adee233e68443f038 Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Tue, 30 Jul 2013 19:33:37 +0200 Subject: [PATCH 024/541] BAP-1278: Control buttons on record level - Implement functionality - Cover by Unit test - Add new functionality to email templates grid --- .../Datagrid/EmailTemplateDatagridManager.php | 9 ++++ .../Property/ActionConfigurationProperty.php | 40 ++++++++++++++++++ .../public/js/app/datagrid/action/cell.js | 19 ++++++++- .../ActionConfigurationPropertyTest.php | 42 +++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php create mode 100644 src/Oro/Bundle/GridBundle/Tests/Unit/Property/ActionConfigurationPropertyTest.php diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php index 87d2a23dfab..42adbc6f474 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php @@ -4,6 +4,8 @@ use Doctrine\ORM\QueryBuilder; +use Oro\Bundle\GridBundle\Datagrid\ResultRecordInterface; +use Oro\Bundle\GridBundle\Property\ActionConfigurationProperty; use Oro\Bundle\GridBundle\Property\UrlProperty; use Oro\Bundle\GridBundle\Action\ActionInterface; use Oro\Bundle\GridBundle\Field\FieldDescription; @@ -39,6 +41,13 @@ protected function getProperties() new UrlProperty('update_link', $this->router, 'oro_email_emailtemplate_update', array('id')), new UrlProperty('clone_link', $this->router, 'oro_email_emailtemplate_clone', array('id')), new UrlProperty('delete_link', $this->router, 'oro_api_delete_emailtemplate', array('id')), + new ActionConfigurationProperty( + function (ResultRecordInterface $record) { + if ($record->getValue('isSystem')) { + return array('delete' => false); + } + } + ) ); } diff --git a/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php b/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php new file mode 100644 index 00000000000..7fbfa915b18 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php @@ -0,0 +1,40 @@ +callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return self::PROPERTY_NAME; + } + + /** + * {@inheritdoc} + */ + public function getValue(ResultRecordInterface $record) + { + $result = call_user_func($this->callback, $record); + + return is_array($result) ? $result : array(); + } +} diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js index 0054fa73f1e..8e642b015c2 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js @@ -77,9 +77,18 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ */ createActions: function() { var result = []; + var actions = this.column.get('actions'); - _.each(actions, function(action) { - result.push(this.createAction(action)); + var actionConfiguration = this.model.get('action_configuration'); + _.each(actions, function(action, name) { + // filter available actions for current row + if ( + _.isUndefined(actionConfiguration) + || _.isUndefined(actionConfiguration[name]) + || actionConfiguration[name] + ) { + result.push(this.createAction(action)); + } }, this); return result; @@ -123,6 +132,12 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ * Render cell with actions */ render: function () { + // don't render anything if list of launchers is empty + if (_.isEmpty(this.launchers)) { + this.$el.empty(); + + return this; + } this.$el.empty().append(this.template()); var launchers = this.getLaunchersByIcons(); diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Property/ActionConfigurationPropertyTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Property/ActionConfigurationPropertyTest.php new file mode 100644 index 00000000000..2fccc119a1e --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Property/ActionConfigurationPropertyTest.php @@ -0,0 +1,42 @@ +getMockForAbstractClass('Oro\Bundle\GridBundle\Datagrid\ResultRecordInterface'); + $this->assertInternalType('array', $property->getValue($recordMock)); + $this->assertEquals('action_configuration', $property->getName()); + } + + /** + * @return array + */ + public function callbackProvider() + { + return array( + 'callback returns array' => array( + 'callback' => function (ResultRecordInterface $record) { + return array(); + } + ), + 'callback returns something else' => array( + 'callback' => function (ResultRecordInterface $record) { + return false; + } + ) + ); + } +} From 474d65cbb5b2fd2b2257641db0d6a4d3a3994a3d Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Wed, 31 Jul 2013 09:46:25 +0000 Subject: [PATCH 025/541] BAP-1279: Ability to disable paginator, "All" option to # of records - pass toolbar params from datagrid options --- .../GridBundle/Resources/public/js/app/datagrid/grid.js | 2 +- .../Resources/public/js/app/datagrid/toolbar.js | 9 ++++++--- .../Resources/views/Include/javascript.html.twig | 6 +++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js index e69adcc04bb..af9c3f42904 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js @@ -115,7 +115,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ options.columns.push(this._createActionsColumn()); this.loadingMask = this._createLoadingMask(); - this.toolbar = this._createToolbar(this.toolbarOptions); + this.toolbar = this._createToolbar(_.extend(this.toolbarOptions, options.toolbarOptions)); Backgrid.Grid.prototype.initialize.apply(this, arguments); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js index b183eb7285e..1c6fd01cf65 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js @@ -64,15 +64,18 @@ Oro.Datagrid.Toolbar = Backbone.View.extend({ collection: this.collection }); - this.pageSize = new this.pageSize({ - collection: this.collection - }); + options.pageSize = options.pageSize || {}; + this.pageSize = new this.pageSize( _.extend({}, options.pageSize, { collection: this.collection }) ); this.actionsPanel = new this.actionsPanel(); if (options.actions) { this.actionsPanel.setActions(options.actions); } + if (options.enable == false) { + this.disable(); + } + Backbone.View.prototype.initialize.call(this, options); }, diff --git a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig index 6a24d42d59f..68c624b7d09 100644 --- a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig +++ b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig @@ -94,7 +94,11 @@ {% endfor %} }, entityHint : {{ datagrid.entityHint|capitalize|json_encode|raw }}, - noDataHint : {{ 'oro_grid.no_data_hint %entityHint%'|trans({'%entityHint%': entityHint}, 'OroGridBundle')|json_encode|raw }} + noDataHint : {{ 'oro_grid.no_data_hint %entityHint%'|trans({'%entityHint%': entityHint}, 'OroGridBundle')|json_encode|raw }}, + toolbarOptions: { + enable: false, + pageSize: { items: [10, 20, 50, 100, 'All']} + } }, collection: { inputName: {{ datagrid.name|json_encode|raw }}, From d1773f53e5c76fb376ff7fee0f42fa463a06abcd Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Wed, 31 Jul 2013 14:01:16 +0000 Subject: [PATCH 026/541] BAP-1232: Wrong behaviour on adding new tags to entity - fix assign/unassign tags logic - fix tag onclick to tag search --- .../Bundle/TagBundle/Entity/TagManager.php | 127 ++++++++++++------ .../TagBundle/Resources/config/services.yml | 1 + .../Resources/public/js/views/select2.js | 2 +- 3 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index b77abd4d4f9..ec16f363940 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -8,11 +8,13 @@ use Doctrine\ORM\Query\Expr; use Oro\Bundle\SearchBundle\Engine\ObjectMapper; +use Oro\Bundle\UserBundle\Acl\Manager; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\Security\Core\SecurityContextInterface; class TagManager { + const ACL_RESOURCE_ID_KEY = 'oro_tag_unassign_global'; /** * @var EntityManager */ @@ -38,14 +40,26 @@ class TagManager */ protected $securityContext; - public function __construct(EntityManager $em, $tagClass, $taggingClass, ObjectMapper $mapper, SecurityContextInterface $securityContext) - { + /** + * @var Manager + */ + protected $aclManager; + + public function __construct( + EntityManager $em, + $tagClass, + $taggingClass, + ObjectMapper $mapper, + SecurityContextInterface $securityContext, + Manager $aclManager + ) { $this->em = $em; $this->tagClass = $tagClass; $this->taggingClass = $taggingClass; $this->mapper = $mapper; $this->securityContext = $securityContext; + $this->aclManager = $aclManager; } /** @@ -65,7 +79,7 @@ public function addTag(Tag $tag, Taggable $resource) * @param Tag[] $tags Array of Tag objects * @param Taggable $resource Taggable resource */ - public function addTags(array $tags, Taggable $resource) + public function addTags($tags, Taggable $resource) { foreach ($tags as $tag) { if ($tag instanceof Tag) { @@ -86,19 +100,6 @@ public function removeTag(Tag $tag, Taggable $resource) return $resource->getTags()->removeElement($tag); } - /** - * Replaces all current tags on the given taggable resource - * - * @param Tag[] $tags Array of Tag objects - * @param Taggable $resource Taggable resource - */ - public function replaceTags(array $tags, Taggable $resource) - { - $resource->setTags(new ArrayCollection()); - - $this->addTags($tags, $resource); - } - /** * Loads or creates multiples tags from a list of tag names * @@ -151,42 +152,70 @@ public function loadOrCreateTags(array $names) */ public function saveTagging(Taggable $resource) { - $oldTags = $this->getTagging($resource); + $oldTags = $this->getTagging($resource, $this->getUser()->getId()); $newTags = $resource->getTags(); if (!isset($newTags['all'], $newTags['owner'])) { return; } - $tagsToAdd = new ArrayCollection($newTags['owner']); + // allow adding only 'my' tags + $newOwnerTags = new ArrayCollection($newTags['owner']); + + // find new + $tagsToAdd = new ArrayCollection(); + foreach ($newOwnerTags as $newOwnerTag) { + $callback = function ($index, $oldTag) use ($newOwnerTag) { + return $oldTag->getName() == $newOwnerTag->getName(); + }; + + if (!$oldTags->exists($callback)) { + $tagsToAdd->add($newOwnerTag); + } + } - if ($oldTags !== null and is_array($oldTags) and !empty($oldTags)) { - $tagsToRemove = array(); + // find removed + $tagsToRemove = array(); + foreach ($oldTags as $oldTag) { + $callback = function ($index, $newTag) use ($oldTag) { + return $newTag->getName() == $oldTag->getName(); + }; + + if (!$newOwnerTags->exists($callback)) { + $tagsToRemove[] = $oldTag->getId(); + } + } + + // process if current user allowed to remove other's tag links + if ($this->aclManager->isResourceGranted(self::ACL_RESOURCE_ID_KEY)) { + $newAllTags = new ArrayCollection($newTags['all']); foreach ($oldTags as $oldTag) { $callback = function ($index, $newTag) use ($oldTag) { return $newTag->getName() == $oldTag->getName(); }; - if ($tagsToAdd->exists($callback)) { - $tagsToAdd->removeElement($oldTag); - } else { + if (!$newAllTags->exists($callback)) { $tagsToRemove[] = $oldTag->getId(); } } + } - if (sizeof($tagsToRemove)) { - $builder = $this->em->createQueryBuilder(); - $builder - ->delete($this->taggingClass, 't') - ->where($builder->expr()->in('t.tag', $tagsToRemove)) - ->andWhere('t.entityName = :entityName') - ->setParameter('entityName', get_class($resource)) - ->andWhere('t.recordId = :recordId') - ->setParameter('recordId', $resource->getTaggableId()) - ->getQuery() - ->getResult(); - } + if (sizeof($tagsToRemove)) { + $builder = $this->em->createQueryBuilder(); + $builder + ->delete($this->taggingClass, 't') + ->where($builder->expr()->in('t.tag', $tagsToRemove)) + ->andWhere('t.entityName = :entityName') + ->setParameter('entityName', get_class($resource)) + ->andWhere('t.recordId = :recordId') + ->setParameter('recordId', $resource->getTaggableId()) + + ->andWhere('t.createdBy = :createdBy') + ->setParameter('createdBy', $this->getUser()->getId()) + + ->getQuery() + ->getResult(); } foreach ($tagsToAdd as $tag) { @@ -213,18 +242,20 @@ public function saveTagging(Taggable $resource) public function loadTagging(Taggable $resource) { $tags = $this->getTagging($resource); - $this->replaceTags($tags, $resource); + $resource->setTags(new ArrayCollection()); + $this->addTags($tags, $resource); } /** * Gets all tags for the given taggable resource * * @param Taggable $resource Taggable resource + * @param null|int $createdBy * @return array */ - protected function getTagging(Taggable $resource) + protected function getTagging(Taggable $resource, $createdBy = null) { - $query = $this->em + $qb = $this->em ->createQueryBuilder() ->select('t') @@ -232,10 +263,14 @@ protected function getTagging(Taggable $resource) ->innerJoin('t.tagging', 't2', Join::WITH, 't2.recordId = :recordId AND t2.entityName = :entityName') ->setParameter('recordId', $resource->getTaggableId()) - ->setParameter('entityName', get_class($resource)) - ->getQuery(); + ->setParameter('entityName', get_class($resource)); - return $query->getResult(); + if (!is_null($createdBy)) { + $qb->where('t2.createdBy = :createdBy') + ->setParameter('createdBy', $createdBy); + } + + return new ArrayCollection($qb->getQuery()->getResult()); } /** @@ -289,4 +324,14 @@ protected function createTagging(Tag $tag, Taggable $resource) { return new $this->taggingClass($tag, $resource); } + + /** + * Return current user + * + * @return mixed + */ + public function getUser() + { + return $this->securityContext->getToken()->getUser(); + } } diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index 4e912b5fa35..25a9fc4770c 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -27,6 +27,7 @@ services: - %oro_tag.tagging.entity.class% - @oro_search.mapper - @security.context + - @oro_user.acl_manager oro_tag.docrine.event.listener: class: %oro_tag.tag_listener.class% diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js index 2e1961d38b1..3dc277bb831 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js @@ -70,7 +70,7 @@ Oro.Tags.Select2View = Oro.Tags.TagView.extend({ tagCollection.add(this.getCollection('owner').models); $('.select2-search-choice div').click(function(){ - var tagName = $(this).attr('title'); + var tagName = $(this).html(); var tag = tagCollection.toArray().filter(function(item){ return item.name == tagName }) var url = tag[0].url; From 95b18c63858501ab12c0f5fe9398cd36e31f2306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gildas=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 31 Jul 2013 16:26:53 +0200 Subject: [PATCH 027/541] [DataAuditBundle] Prevent trying to format a date that is null --- src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php index 679b66341b1..150bc81da97 100644 --- a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php +++ b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php @@ -422,7 +422,9 @@ function ($item) { ); } elseif ($newData instanceof \DateTime) { - $oldData = $oldData->format(\DateTime::ISO8601); + if ($oldData instanceof \DateTime) { + $oldData = $oldData->format(\DateTime::ISO8601); + } $newData = $newData->format(\DateTime::ISO8601); } elseif (is_object($newData)) { From ffaf9bcfe5e315adf3769997e5bd3c193cb02c96 Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Wed, 31 Jul 2013 18:11:36 +0200 Subject: [PATCH 028/541] BAP-1233: Wrong behavior on adding new tags during creating new entity - Code style improvements - Added not minified version of select2 --- .../Resources/public/js/views/tag.js | 22 +- .../Resources/views/Form/fields.html.twig | 29 +- .../Resources/views/javascript.html.twig | 15 - .../Resources/views/macros.html.twig | 8 +- .../UIBundle/Resources/config/assets.yml | 2 +- .../Resources/public/lib/jquery/select2.js | 3137 +++++++++++++++++ 6 files changed, 3168 insertions(+), 45 deletions(-) delete mode 100644 src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js index d649468df32..9b6c1446b6d 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js @@ -6,6 +6,19 @@ Oro.Tags.TagView = Backbone.View.extend({ filter: null }, + /** @property */ + template:_.template( + '' + ), + /** * Constructor */ @@ -14,8 +27,6 @@ Oro.Tags.TagView = Backbone.View.extend({ this.listenTo(this.getCollection(), 'reset', this.render); this.listenTo(this, 'filter', this.render); - this.template = $('#tag-view-template').html(); - // process filter action binding $('#tag-sort-actions a').click(_.bind(this.filter, this)); }, @@ -55,12 +66,7 @@ Oro.Tags.TagView = Backbone.View.extend({ */ render: function() { $('#tags-holder').html( - _.template( - this.template, - { - "collection": this.getCollection().getFilteredCollection(this.options.filter) - } - ) + this.template(this.getCollection().getFilteredCollection(this.options.filter)) ); // process tag click redirect if (Oro.hashNavigationEnabled()) { diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index 50df37ba5e9..27735bda400 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -1,19 +1,20 @@ {% block oro_combobox_dataconfig_multi_autocomplete %} {{ block('oro_combobox_dataconfig_autocomplete') }} - select2Config.multiple = true; select2Config.createSearchChoice = function(term, data) { - if ($(data).filter(function() { - return this.name.localeCompare(term) === 0; - }).length === 0) { - {% if not resource_granted('oro_tag_create') %} - return null; - {% else %} - return { - id: term, - name: term - }; - {% endif %} + if ( + $(data).filter(function() { + return this.name.localeCompare(term) === 0; + }).length === 0 + ) { + {% if not resource_granted('oro_tag_create') %} + return null; + {% else %} + return { + id: term, + name: term + }; + {% endif %} + } } - } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig deleted file mode 100644 index 0f7e787c7d8..00000000000 --- a/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig +++ /dev/null @@ -1,15 +0,0 @@ -{% block oro_tag_view_js %} - -{% endblock oro_tag_view_js %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig index bb1f6d7fedc..036378b7754 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig @@ -1,11 +1,4 @@ {% macro renderView(entity) %} - {# // check if block was not included yet #} - {% set _block = block('oro_tag_view_js') %} - {% if (_block is empty) %} - {% include 'OroTagBundle::javascript.html.twig' %} - {{ block('oro_tag_view_js')|raw }} - {% endif %} -
{% import _self as _ %} @@ -48,6 +41,7 @@ new Oro.Tags.Select2View({tagInputId: '#{{ form.tags.vars.id }}', tags: tags}); {% if not resource_granted('oro_tag_assign_unassign') %} + $('#tag-sort-actions').hide(); $('#{{ form.tags.vars.id }}').select2("readonly", true); {% endif %} }); diff --git a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml index 724fc254d0a..bb10f482863 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml @@ -14,7 +14,7 @@ js: - '@OroUIBundle/Resources/public/lib/jquery/jquery.mousewheel.min.js' - '@OroUIBundle/Resources/public/lib/jquery/jquery.numeric.js' - '@OroUIBundle/Resources/public/lib/jquery/jquery.uniform.min.js' - - '@OroUIBundle/Resources/public/lib/jquery/select2.min.js' + - '@OroUIBundle/Resources/public/lib/jquery/select2.js' 'bootstrap': - '@OroUIBundle/Resources/public/lib/bootstrap.min.js' - '@OroUIBundle/Resources/public/lib/bootstrap/js/bootstrap-tooltip.js' diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js new file mode 100644 index 00000000000..a4960714c2b --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js @@ -0,0 +1,3137 @@ +/* +Copyright 2012 Igor Vaynberg + +Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + + http://www.apache.org/licenses/LICENSE-2.0 + http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the +Apache License or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for +the specific language governing permissions and limitations under the Apache License and the GPL License. +*/ +(function ($) { + if(typeof $.fn.each2 == "undefined") { + $.fn.extend({ + /* + * 4-10 times faster .each replacement + * use it carefully, as it overrides jQuery context of element on each iteration + */ + each2 : function (c) { + var j = $([0]), i = -1, l = this.length; + while ( + ++i < l + && (j.context = j[0] = this[i]) + && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object + ); + return this; + } + }); + } +})(jQuery); + +(function ($, undefined) { + "use strict"; + /*global document, window, jQuery, console */ + + if (window.Select2 !== undefined) { + return; + } + + var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, + lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, + + KEY = { + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + SHIFT: 16, + CTRL: 17, + ALT: 18, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + BACKSPACE: 8, + DELETE: 46, + isArrow: function (k) { + k = k.which ? k.which : k; + switch (k) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + return true; + } + return false; + }, + isControl: function (e) { + var k = e.which; + switch (k) { + case KEY.SHIFT: + case KEY.CTRL: + case KEY.ALT: + return true; + } + + if (e.metaKey) return true; + + return false; + }, + isFunctionKey: function (k) { + k = k.which ? k.which : k; + return k >= 112 && k <= 123; + } + }, + MEASURE_SCROLLBAR_TEMPLATE = "
"; + + $document = $(document); + + nextUid=(function() { var counter=1; return function() { return counter++; }; }()); + + function indexOf(value, array) { + var i = 0, l = array.length; + for (; i < l; i = i + 1) { + if (equal(value, array[i])) return i; + } + return -1; + } + + function measureScrollbar () { + var $template = $( MEASURE_SCROLLBAR_TEMPLATE ); + $template.appendTo('body'); + + var dim = { + width: $template.width() - $template[0].clientWidth, + height: $template.height() - $template[0].clientHeight + }; + $template.remove(); + + return dim; + } + + /** + * Compares equality of a and b + * @param a + * @param b + */ + function equal(a, b) { + if (a === b) return true; + if (a === undefined || b === undefined) return false; + if (a === null || b === null) return false; + // Check whether 'a' or 'b' is a string (primitive or object). + // The concatenation of an empty string (+'') converts its argument to a string's primitive. + if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object + if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object + return false; + } + + /** + * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty + * strings + * @param string + * @param separator + */ + function splitVal(string, separator) { + var val, i, l; + if (string === null || string.length < 1) return []; + val = string.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); + return val; + } + + function getSideBorderPadding(element) { + return element.outerWidth(false) - element.width(); + } + + function installKeyUpChangeEvent(element) { + var key="keyup-change-value"; + element.on("keydown", function () { + if ($.data(element, key) === undefined) { + $.data(element, key, element.val()); + } + }); + element.on("keyup", function () { + var val= $.data(element, key); + if (val !== undefined && element.val() !== val) { + $.removeData(element, key); + element.trigger("keyup-change"); + } + }); + } + + $document.on("mousemove", function (e) { + lastMousePosition.x = e.pageX; + lastMousePosition.y = e.pageY; + }); + + /** + * filters mouse events so an event is fired only if the mouse moved. + * + * filters out mouse events that occur when mouse is stationary but + * the elements under the pointer are scrolled. + */ + function installFilteredMouseMove(element) { + element.on("mousemove", function (e) { + var lastpos = lastMousePosition; + if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { + $(e.target).trigger("mousemove-filtered", e); + } + }); + } + + /** + * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made + * within the last quietMillis milliseconds. + * + * @param quietMillis number of milliseconds to wait before invoking fn + * @param fn function to be debounced + * @param ctx object to be used as this reference within fn + * @return debounced version of fn + */ + function debounce(quietMillis, fn, ctx) { + ctx = ctx || undefined; + var timeout; + return function () { + var args = arguments; + window.clearTimeout(timeout); + timeout = window.setTimeout(function() { + fn.apply(ctx, args); + }, quietMillis); + }; + } + + /** + * A simple implementation of a thunk + * @param formula function used to lazily initialize the thunk + * @return {Function} + */ + function thunk(formula) { + var evaluated = false, + value; + return function() { + if (evaluated === false) { value = formula(); evaluated = true; } + return value; + }; + }; + + function installDebouncedScroll(threshold, element) { + var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); + element.on("scroll", function (e) { + if (indexOf(e.target, element.get()) >= 0) notify(e); + }); + } + + function focus($el) { + if ($el[0] === document.activeElement) return; + + /* set the focus in a 0 timeout - that way the focus is set after the processing + of the current event has finished - which seems like the only reliable way + to set focus */ + window.setTimeout(function() { + var el=$el[0], pos=$el.val().length, range; + + $el.focus(); + + /* make sure el received focus so we do not error out when trying to manipulate the caret. + sometimes modals or others listeners may steal it after its set */ + if ($el.is(":visible") && el === document.activeElement) { + + /* after the focus is set move the caret to the end, necessary when we val() + just before setting focus */ + if(el.setSelectionRange) + { + el.setSelectionRange(pos, pos); + } + else if (el.createTextRange) { + range = el.createTextRange(); + range.collapse(false); + range.select(); + } + } + }, 0); + } + + function getCursorInfo(el) { + el = $(el)[0]; + var offset = 0; + var length = 0; + if ('selectionStart' in el) { + offset = el.selectionStart; + length = el.selectionEnd - offset; + } else if ('selection' in document) { + el.focus(); + var sel = document.selection.createRange(); + length = document.selection.createRange().text.length; + sel.moveStart('character', -el.value.length); + offset = sel.text.length - length; + } + return { offset: offset, length: length }; + } + + function killEvent(event) { + event.preventDefault(); + event.stopPropagation(); + } + function killEventImmediately(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + + function measureTextWidth(e) { + if (!sizer){ + var style = e[0].currentStyle || window.getComputedStyle(e[0], null); + sizer = $(document.createElement("div")).css({ + position: "absolute", + left: "-10000px", + top: "-10000px", + display: "none", + fontSize: style.fontSize, + fontFamily: style.fontFamily, + fontStyle: style.fontStyle, + fontWeight: style.fontWeight, + letterSpacing: style.letterSpacing, + textTransform: style.textTransform, + whiteSpace: "nowrap" + }); + sizer.attr("class","select2-sizer"); + $("body").append(sizer); + } + sizer.text(e.val()); + return sizer.width(); + } + + function syncCssClasses(dest, src, adapter) { + var classes, replacements = [], adapted; + + classes = dest.attr("class"); + if (classes) { + classes = '' + classes; // for IE which returns object + $(classes.split(" ")).each2(function() { + if (this.indexOf("select2-") === 0) { + replacements.push(this); + } + }); + } + classes = src.attr("class"); + if (classes) { + classes = '' + classes; // for IE which returns object + $(classes.split(" ")).each2(function() { + if (this.indexOf("select2-") !== 0) { + adapted = adapter(this); + if (adapted) { + replacements.push(this); + } + } + }); + } + dest.attr("class", replacements.join(" ")); + } + + + function markMatch(text, term, markup, escapeMarkup) { + var match=text.toUpperCase().indexOf(term.toUpperCase()), + tl=term.length; + + if (match<0) { + markup.push(escapeMarkup(text)); + return; + } + + markup.push(escapeMarkup(text.substring(0, match))); + markup.push(""); + markup.push(escapeMarkup(text.substring(match, match + tl))); + markup.push(""); + markup.push(escapeMarkup(text.substring(match + tl, text.length))); + } + + function defaultEscapeMarkup(markup) { + var replace_map = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + "/": '/' + }; + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replace_map[match]; + }); + } + + /** + * Produces an ajax-based query function + * + * @param options object containing configuration paramters + * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax + * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax + * @param options.url url for the data + * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. + * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified + * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often + * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. + * The expected format is an object containing the following keys: + * results array of objects that will be used as choices + * more (optional) boolean indicating whether there are more results available + * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} + */ + function ajax(options) { + var timeout, // current scheduled but not yet executed request + requestSequence = 0, // sequence used to drop out-of-order responses + handler = null, + quietMillis = options.quietMillis || 100, + ajaxUrl = options.url, + self = this; + + return function (query) { + window.clearTimeout(timeout); + timeout = window.setTimeout(function () { + requestSequence += 1; // increment the sequence + var requestNumber = requestSequence, // this request's sequence number + data = options.data, // ajax data function + url = ajaxUrl, // ajax url string or function + transport = options.transport || $.fn.select2.ajaxDefaults.transport, + // deprecated - to be removed in 4.0 - use params instead + deprecated = { + type: options.type || 'GET', // set type of request (GET or POST) + cache: options.cache || false, + jsonpCallback: options.jsonpCallback||undefined, + dataType: options.dataType||"json" + }, + params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated); + + data = data ? data.call(self, query.term, query.page, query.context) : null; + url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; + + if (handler) { handler.abort(); } + + if (options.params) { + if ($.isFunction(options.params)) { + $.extend(params, options.params.call(self)); + } else { + $.extend(params, options.params); + } + } + + $.extend(params, { + url: url, + dataType: options.dataType, + data: data, + success: function (data) { + if (requestNumber < requestSequence) { + return; + } + // TODO - replace query.page with query so users have access to term, page, etc. + var results = options.results(data, query.page); + query.callback(results); + } + }); + handler = transport.call(self, params); + }, quietMillis); + }; + } + + /** + * Produces a query function that works with a local array + * + * @param options object containing configuration parameters. The options parameter can either be an array or an + * object. + * + * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. + * + * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain + * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' + * key can either be a String in which case it is expected that each element in the 'data' array has a key with the + * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract + * the text. + */ + function local(options) { + var data = options, // data elements + dataText, + tmp, + text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search + + if ($.isArray(data)) { + tmp = data; + data = { results: tmp }; + } + + if ($.isFunction(data) === false) { + tmp = data; + data = function() { return tmp; }; + } + + var dataItem = data(); + if (dataItem.text) { + text = dataItem.text; + // if text is not a function we assume it to be a key name + if (!$.isFunction(text)) { + dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available + text = function (item) { return item[dataText]; }; + } + } + + return function (query) { + var t = query.term, filtered = { results: [] }, process; + if (t === "") { + query.callback(data()); + return; + } + + process = function(datum, collection) { + var group, attr; + datum = datum[0]; + if (datum.children) { + group = {}; + for (attr in datum) { + if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; + } + group.children=[]; + $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); + if (group.children.length || query.matcher(t, text(group), datum)) { + collection.push(group); + } + } else { + if (query.matcher(t, text(datum), datum)) { + collection.push(datum); + } + } + }; + + $(data().results).each2(function(i, datum) { process(datum, filtered.results); }); + query.callback(filtered); + }; + } + + // TODO javadoc + function tags(data) { + var isFunc = $.isFunction(data); + return function (query) { + var t = query.term, filtered = {results: []}; + $(isFunc ? data() : data).each(function () { + var isObject = this.text !== undefined, + text = isObject ? this.text : this; + if (t === "" || query.matcher(t, text)) { + filtered.results.push(isObject ? this : {id: this, text: this}); + } + }); + query.callback(filtered); + }; + } + + /** + * Checks if the formatter function should be used. + * + * Throws an error if it is not a function. Returns true if it should be used, + * false if no formatting should be performed. + * + * @param formatter + */ + function checkFormatter(formatter, formatterName) { + if ($.isFunction(formatter)) return true; + if (!formatter) return false; + throw new Error(formatterName +" must be a function or a falsy value"); + } + + function evaluate(val) { + return $.isFunction(val) ? val() : val; + } + + function countResults(results) { + var count = 0; + $.each(results, function(i, item) { + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + }); + return count; + } + + /** + * Default tokenizer. This function uses breaks the input on substring match of any string from the + * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those + * two options have to be defined in order for the tokenizer to work. + * + * @param input text user has typed so far or pasted into the search field + * @param selection currently selected choices + * @param selectCallback function(choice) callback tho add the choice to selection + * @param opts select2's opts + * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value + */ + function defaultTokenizer(input, selection, selectCallback, opts) { + var original = input, // store the original so we can compare and know if we need to tell the search to update its text + dupe = false, // check for whether a token we extracted represents a duplicate selected choice + token, // token + index, // position at which the separator was found + i, l, // looping variables + separator; // the matched separator + + if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; + + while (true) { + index = -1; + + for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { + separator = opts.tokenSeparators[i]; + index = input.indexOf(separator); + if (index >= 0) break; + } + + if (index < 0) break; // did not find any token separator in the input string, bail + + token = input.substring(0, index); + input = input.substring(index + separator.length); + + if (token.length > 0) { + token = opts.createSearchChoice.call(this, token, selection); + if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { + dupe = false; + for (i = 0, l = selection.length; i < l; i++) { + if (equal(opts.id(token), opts.id(selection[i]))) { + dupe = true; break; + } + } + + if (!dupe) selectCallback(token); + } + } + } + + if (original!==input) return input; + } + + /** + * Creates a new class + * + * @param superClass + * @param methods + */ + function clazz(SuperClass, methods) { + var constructor = function () {}; + constructor.prototype = new SuperClass; + constructor.prototype.constructor = constructor; + constructor.prototype.parent = SuperClass.prototype; + constructor.prototype = $.extend(constructor.prototype, methods); + return constructor; + } + + AbstractSelect2 = clazz(Object, { + + // abstract + bind: function (func) { + var self = this; + return function () { + func.apply(self, arguments); + }; + }, + + // abstract + init: function (opts) { + var results, search, resultsSelector = ".select2-results", disabled, readonly; + + // prepare options + this.opts = opts = this.prepareOpts(opts); + + this.id=opts.id; + + // destroy if called on an existing component + if (opts.element.data("select2") !== undefined && + opts.element.data("select2") !== null) { + opts.element.data("select2").destroy(); + } + + this.container = this.createContainer(); + + this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); + this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); + this.container.attr("id", this.containerId); + + // cache the body so future lookups are cheap + this.body = thunk(function() { return opts.element.closest("body"); }); + + syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); + + this.container.css(evaluate(opts.containerCss)); + this.container.addClass(evaluate(opts.containerCssClass)); + + this.elementTabIndex = this.opts.element.attr("tabindex"); + + // swap container for the element + this.opts.element + .data("select2", this) + .attr("tabindex", "-1") + .before(this.container); + this.container.data("select2", this); + + this.dropdown = this.container.find(".select2-drop"); + this.dropdown.addClass(evaluate(opts.dropdownCssClass)); + this.dropdown.data("select2", this); + + this.results = results = this.container.find(resultsSelector); + this.search = search = this.container.find("input.select2-input"); + + this.resultsPage = 0; + this.context = null; + + // initialize the container + this.initContainer(); + + installFilteredMouseMove(this.results); + this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent)); + + installDebouncedScroll(80, this.results); + this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded)); + + // do not propagate change event from the search field out of the component + $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();}); + $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();}); + + // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel + if ($.fn.mousewheel) { + results.mousewheel(function (e, delta, deltaX, deltaY) { + var top = results.scrollTop(), height; + if (deltaY > 0 && top - deltaY <= 0) { + results.scrollTop(0); + killEvent(e); + } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { + results.scrollTop(results.get(0).scrollHeight - results.height()); + killEvent(e); + } + }); + } + + installKeyUpChangeEvent(search); + search.on("keyup-change input paste", this.bind(this.updateResults)); + search.on("focus", function () { search.addClass("select2-focused"); }); + search.on("blur", function () { search.removeClass("select2-focused");}); + + this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) { + if ($(e.target).closest(".select2-result-selectable").length > 0) { + this.highlightUnderEvent(e); + this.selectHighlighted(e); + } + })); + + // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening + // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's + // dom it will trigger the popup close, which is not what we want + this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); }); + + if ($.isFunction(this.opts.initSelection)) { + // initialize selection based on the current value of the source element + this.initSelection(); + + // if the user has provided a function that can set selection based on the value of the source element + // we monitor the change event on the element and trigger it, allowing for two way synchronization + this.monitorSource(); + } + + if (opts.maximumInputLength !== null) { + this.search.attr("maxlength", opts.maximumInputLength); + } + + var disabled = opts.element.prop("disabled"); + if (disabled === undefined) disabled = false; + this.enable(!disabled); + + var readonly = opts.element.prop("readonly"); + if (readonly === undefined) readonly = false; + this.readonly(readonly); + + // Calculate size of scrollbar + scrollBarDimensions = scrollBarDimensions || measureScrollbar(); + + this.autofocus = opts.element.prop("autofocus") + opts.element.prop("autofocus", false); + if (this.autofocus) this.focus(); + }, + + // abstract + destroy: function () { + var element=this.opts.element, select2 = element.data("select2"); + + if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } + + if (select2 !== undefined) { + select2.container.remove(); + select2.dropdown.remove(); + element + .removeClass("select2-offscreen") + .removeData("select2") + .off(".select2") + .prop("autofocus", this.autofocus || false); + if (this.elementTabIndex) { + element.attr({tabindex: this.elementTabIndex}); + } else { + element.removeAttr("tabindex"); + } + element.show(); + } + }, + + // abstract + optionToData: function(element) { + if (element.is("option")) { + return { + id:element.prop("value"), + text:element.text(), + element: element.get(), + css: element.attr("class"), + disabled: element.prop("disabled"), + locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true) + }; + } else if (element.is("optgroup")) { + return { + text:element.attr("label"), + children:[], + element: element.get(), + css: element.attr("class") + }; + } + }, + + // abstract + prepareOpts: function (opts) { + var element, select, idKey, ajaxUrl, self = this; + + element = opts.element; + + if (element.get(0).tagName.toLowerCase() === "select") { + this.select = select = opts.element; + } + + if (select) { + // these options are not allowed when attached to a select because they are picked up off the element itself + $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { + if (this in opts) { + throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a ", + "
", + " ", + "
    ", + "
", + "
"].join("")); + return container; + }, + + // single + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.focusser.prop("disabled", !this.isInterfaceEnabled()); + } + }, + + // single + opening: function () { + var el, range, len; + + if (this.opts.minimumResultsForSearch >= 0) { + this.showSearch(true); + } + + this.parent.opening.apply(this, arguments); + + if (this.showSearchInput !== false) { + // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range + // all other browsers handle this just fine + + this.search.val(this.focusser.val()); + } + this.search.focus(); + // move the cursor to the end after focussing, otherwise it will be at the beginning and + // new text will appear *before* focusser.val() + el = this.search.get(0); + if (el.createTextRange) { + range = el.createTextRange(); + range.collapse(false); + range.select(); + } else if (el.setSelectionRange) { + len = this.search.val().length; + el.setSelectionRange(len, len); + } + + this.focusser.prop("disabled", true).val(""); + this.updateResults(true); + this.opts.element.trigger($.Event("select2-open")); + }, + + // single + close: function () { + if (!this.opened()) return; + this.parent.close.apply(this, arguments); + this.focusser.removeAttr("disabled"); + this.focusser.focus(); + }, + + // single + focus: function () { + if (this.opened()) { + this.close(); + } else { + this.focusser.removeAttr("disabled"); + this.focusser.focus(); + } + }, + + // single + isFocused: function () { + return this.container.hasClass("select2-container-active"); + }, + + // single + cancel: function () { + this.parent.cancel.apply(this, arguments); + this.focusser.removeAttr("disabled"); + this.focusser.focus(); + }, + + // single + initContainer: function () { + + var selection, + container = this.container, + dropdown = this.dropdown; + + if (this.opts.minimumResultsForSearch < 0) { + this.showSearch(false); + } else { + this.showSearch(true); + } + + this.selection = selection = container.find(".select2-choice"); + + this.focusser = container.find(".select2-focusser"); + + // rewrite labels from original element to focusser + this.focusser.attr("id", "s2id_autogen"+nextUid()); + + $("label[for='" + this.opts.element.attr("id") + "']") + .attr('for', this.focusser.attr('id')); + + this.focusser.attr("tabindex", this.elementTabIndex); + + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { + // prevent the page from scrolling + killEvent(e); + return; + } + + switch (e.which) { + case KEY.UP: + case KEY.DOWN: + this.moveHighlight((e.which === KEY.UP) ? -1 : 1); + killEvent(e); + return; + case KEY.ENTER: + this.selectHighlighted(); + killEvent(e); + return; + case KEY.TAB: + this.selectHighlighted({noFocus: true}); + return; + case KEY.ESC: + this.cancel(e); + killEvent(e); + return; + } + })); + + this.search.on("blur", this.bind(function(e) { + // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. + // without this the search field loses focus which is annoying + if (document.activeElement === this.body().get(0)) { + window.setTimeout(this.bind(function() { + this.search.focus(); + }), 0); + } + })); + + this.focusser.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { + return; + } + + if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { + killEvent(e); + return; + } + + if (e.which == KEY.DOWN || e.which == KEY.UP + || (e.which == KEY.ENTER && this.opts.openOnEnter)) { + + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; + + this.open(); + killEvent(e); + return; + } + + if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) { + if (this.opts.allowClear) { + this.clear(); + } + killEvent(e); + return; + } + })); + + + installKeyUpChangeEvent(this.focusser); + this.focusser.on("keyup-change input", this.bind(function(e) { + if (this.opts.minimumResultsForSearch >= 0) { + e.stopPropagation(); + if (this.opened()) return; + this.open(); + } + })); + + selection.on("mousedown", "abbr", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + this.clear(); + killEventImmediately(e); + this.close(); + this.selection.focus(); + })); + + selection.on("mousedown", this.bind(function (e) { + + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + + if (this.opened()) { + this.close(); + } else if (this.isInterfaceEnabled()) { + this.open(); + } + + killEvent(e); + })); + + dropdown.on("mousedown", this.bind(function() { this.search.focus(); })); + + selection.on("focus", this.bind(function(e) { + killEvent(e); + })); + + this.focusser.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + })).on("blur", this.bind(function() { + if (!this.opened()) { + this.container.removeClass("select2-container-active"); + this.opts.element.trigger($.Event("select2-blur")); + } + })); + this.search.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + })); + + this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); + this.setPlaceholder(); + + }, + + // single + clear: function(triggerChange) { + var data=this.selection.data("select2-data"); + if (data) { // guard against queued quick consecutive clicks + var placeholderOption = this.getPlaceholderOption(); + this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); + this.selection.find(".select2-chosen").empty(); + this.selection.removeData("select2-data"); + this.setPlaceholder(); + + if (triggerChange !== false){ + this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); + this.triggerChange({removed:data}); + } + } + }, + + /** + * Sets selection based on source element's value + */ + // single + initSelection: function () { + var selected; + if (this.isPlaceholderOptionSelected()) { + this.updateSelection([]); + this.close(); + this.setPlaceholder(); + } else { + var self = this; + this.opts.initSelection.call(null, this.opts.element, function(selected){ + if (selected !== undefined && selected !== null) { + self.updateSelection(selected); + self.close(); + self.setPlaceholder(); + } + }); + } + }, + + isPlaceholderOptionSelected: function() { + var placeholderOption; + return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.is(':selected')) || + (this.opts.element.val() === "") || + (this.opts.element.val() === undefined) || + (this.opts.element.val() === null); + }, + + // single + prepareOpts: function () { + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + // install the selection initializer + opts.initSelection = function (element, callback) { + var selected = element.find(":selected"); + // a single select box always has a value, no need to null check 'selected' + callback(self.optionToData(selected)); + }; + } else if ("data" in opts) { + // install default initSelection when applied to hidden input and data is local + opts.initSelection = opts.initSelection || function (element, callback) { + var id = element.val(); + //search in data by id, storing the actual matching item + var match = null; + opts.query({ + matcher: function(term, text, el){ + var is_match = equal(id, opts.id(el)); + if (is_match) { + match = el; + } + return is_match; + }, + callback: !$.isFunction(callback) ? $.noop : function() { + callback(match); + } + }); + }; + } + + return opts; + }, + + // single + getPlaceholder: function() { + // if a placeholder is specified on a single select without a valid placeholder option ignore it + if (this.select) { + if (this.getPlaceholderOption() === undefined) { + return undefined; + } + } + + return this.parent.getPlaceholder.apply(this, arguments); + }, + + // single + setPlaceholder: function () { + var placeholder = this.getPlaceholder(); + + if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { + + // check for a placeholder option if attached to a select + if (this.select && this.getPlaceholderOption() === undefined) return; + + this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder)); + + this.selection.addClass("select2-default"); + + this.container.removeClass("select2-allowclear"); + } + }, + + // single + postprocessResults: function (data, initial, noHighlightUpdate) { + var selected = 0, self = this, showSearchInput = true; + + // find the selected element in the result list + + this.findHighlightableChoices().each2(function (i, elm) { + if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { + selected = i; + return false; + } + }); + + // and highlight it + if (noHighlightUpdate !== false) { + if (initial === true && selected >= 0) { + this.highlight(selected); + } else { + this.highlight(0); + } + } + + // hide the search box if this is the first we got the results and there are enough of them for search + + if (initial === true) { + var min = this.opts.minimumResultsForSearch; + if (min >= 0) { + this.showSearch(countResults(data.results) >= min); + } + } + }, + + // single + showSearch: function(showSearchInput) { + if (this.showSearchInput === showSearchInput) return; + + this.showSearchInput = showSearchInput; + + this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); + this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); + //add "select2-with-searchbox" to the container if search box is shown + $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); + }, + + // single + onSelect: function (data, options) { + + if (!this.triggerSelect(data)) { return; } + + var old = this.opts.element.val(), + oldData = this.data(); + + this.opts.element.val(this.id(data)); + this.updateSelection(data); + + this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); + + this.close(); + + if (!options || !options.noFocus) + this.selection.focus(); + + if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); } + }, + + // single + updateSelection: function (data) { + + var container=this.selection.find(".select2-chosen"), formatted, cssClass; + + this.selection.data("select2-data", data); + + container.empty(); + formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); + if (formatted !== undefined) { + container.append(formatted); + } + cssClass=this.opts.formatSelectionCssClass(data, container); + if (cssClass !== undefined) { + container.addClass(cssClass); + } + + this.selection.removeClass("select2-default"); + + if (this.opts.allowClear && this.getPlaceholder() !== undefined) { + this.container.addClass("select2-allowclear"); + } + }, + + // single + val: function () { + var val, + triggerChange = false, + data = null, + self = this, + oldData = this.data(); + + if (arguments.length === 0) { + return this.opts.element.val(); + } + + val = arguments[0]; + + if (arguments.length > 1) { + triggerChange = arguments[1]; + } + + if (this.select) { + this.select + .val(val) + .find(":selected").each2(function (i, elm) { + data = self.optionToData(elm); + return false; + }); + this.updateSelection(data); + this.setPlaceholder(); + if (triggerChange) { + this.triggerChange({added: data, removed:oldData}); + } + } else { + // val is an id. !val is true for [undefined,null,'',0] - 0 is legal + if (!val && val !== 0) { + this.clear(triggerChange); + return; + } + if (this.opts.initSelection === undefined) { + throw new Error("cannot call val() if initSelection() is not defined"); + } + this.opts.element.val(val); + this.opts.initSelection(this.opts.element, function(data){ + self.opts.element.val(!data ? "" : self.id(data)); + self.updateSelection(data); + self.setPlaceholder(); + if (triggerChange) { + self.triggerChange({added: data, removed:oldData}); + } + }); + } + }, + + // single + clearSearch: function () { + this.search.val(""); + this.focusser.val(""); + }, + + // single + data: function(value, triggerChange) { + var data; + + if (arguments.length === 0) { + data = this.selection.data("select2-data"); + if (data == undefined) data = null; + return data; + } else { + if (!value || value === "") { + this.clear(triggerChange); + } else { + data = this.data(); + this.opts.element.val(!value ? "" : this.id(value)); + this.updateSelection(value); + if (triggerChange) { + this.triggerChange({added: value, removed:data}); + } + } + } + } + }); + + MultiSelect2 = clazz(AbstractSelect2, { + + // multi + createContainer: function () { + var container = $(document.createElement("div")).attr({ + "class": "select2-container select2-container-multi" + }).html([ + "
    ", + "
  • ", + " ", + "
  • ", + "
", + "
", + "
    ", + "
", + "
"].join("")); + return container; + }, + + // multi + prepareOpts: function () { + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; + + // TODO validate placeholder is a string if specified + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + // install sthe selection initializer + opts.initSelection = function (element, callback) { + + var data = []; + + element.find(":selected").each2(function (i, elm) { + data.push(self.optionToData(elm)); + }); + callback(data); + }; + } else if ("data" in opts) { + // install default initSelection when applied to hidden input and data is local + opts.initSelection = opts.initSelection || function (element, callback) { + var ids = splitVal(element.val(), opts.separator); + //search in data by array of ids, storing matching items in a list + var matches = []; + opts.query({ + matcher: function(term, text, el){ + var is_match = $.grep(ids, function(id) { + return equal(id, opts.id(el)); + }).length; + if (is_match) { + matches.push(el); + } + return is_match; + }, + callback: !$.isFunction(callback) ? $.noop : function() { + // reorder matches based on the order they appear in the ids array because right now + // they are in the order in which they appear in data array + var ordered = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + for (var j = 0; j < matches.length; j++) { + var match = matches[j]; + if (equal(id, opts.id(match))) { + ordered.push(match); + matches.splice(j, 1); + break; + } + } + } + callback(ordered); + } + }); + }; + } + + return opts; + }, + + selectChoice: function (choice) { + + var selected = this.container.find(".select2-search-choice-focus"); + if (selected.length && choice && choice[0] == selected[0]) { + + } else { + if (selected.length) { + this.opts.element.trigger("choice-deselected", selected); + } + selected.removeClass("select2-search-choice-focus"); + if (choice && choice.length) { + this.close(); + choice.addClass("select2-search-choice-focus"); + this.opts.element.trigger("choice-selected", choice); + } + } + }, + + // multi + initContainer: function () { + + var selector = ".select2-choices", selection; + + this.searchContainer = this.container.find(".select2-search-field"); + this.selection = selection = this.container.find(selector); + + var _this = this; + this.selection.on("mousedown", ".select2-search-choice", function (e) { + //killEvent(e); + _this.search[0].focus(); + _this.selectChoice($(this)); + }) + + // rewrite labels from original element to focusser + this.search.attr("id", "s2id_autogen"+nextUid()); + $("label[for='" + this.opts.element.attr("id") + "']") + .attr('for', this.search.attr('id')); + + this.search.on("input paste", this.bind(function() { + if (!this.isInterfaceEnabled()) return; + if (!this.opened()) { + this.open(); + } + })); + + this.search.attr("tabindex", this.elementTabIndex); + + this.keydowns = 0; + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + ++this.keydowns; + var selected = selection.find(".select2-search-choice-focus"); + var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); + var next = selected.next(".select2-search-choice:not(.select2-locked)"); + var pos = getCursorInfo(this.search); + + if (selected.length && + (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { + var selectedChoice = selected; + if (e.which == KEY.LEFT && prev.length) { + selectedChoice = prev; + } + else if (e.which == KEY.RIGHT) { + selectedChoice = next.length ? next : null; + } + else if (e.which === KEY.BACKSPACE) { + this.unselect(selected.first()); + this.search.width(10); + selectedChoice = prev.length ? prev : next; + } else if (e.which == KEY.DELETE) { + this.unselect(selected.first()); + this.search.width(10); + selectedChoice = next.length ? next : null; + } else if (e.which == KEY.ENTER) { + selectedChoice = null; + } + + this.selectChoice(selectedChoice); + killEvent(e); + if (!selectedChoice || !selectedChoice.length) { + this.open(); + } + return; + } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) + || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { + + this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); + killEvent(e); + return; + } else { + this.selectChoice(null); + } + + if (this.opened()) { + switch (e.which) { + case KEY.UP: + case KEY.DOWN: + this.moveHighlight((e.which === KEY.UP) ? -1 : 1); + killEvent(e); + return; + case KEY.ENTER: + this.selectHighlighted(); + killEvent(e); + return; + case KEY.TAB: + this.selectHighlighted({noFocus:true}); + this.close(); + return; + case KEY.ESC: + this.cancel(e); + killEvent(e); + return; + } + } + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) + || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { + return; + } + + if (e.which === KEY.ENTER) { + if (this.opts.openOnEnter === false) { + return; + } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + } + + this.open(); + + if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { + // prevent the page from scrolling + killEvent(e); + } + + if (e.which === KEY.ENTER) { + // prevent form from being submitted + killEvent(e); + } + + })); + + this.search.on("keyup", this.bind(function (e) { + this.keydowns = 0; + this.resizeSearch(); + }) + ); + + this.search.on("blur", this.bind(function(e) { + this.container.removeClass("select2-container-active"); + this.search.removeClass("select2-focused"); + this.selectChoice(null); + if (!this.opened()) this.clearSearch(); + e.stopImmediatePropagation(); + this.opts.element.trigger($.Event("select2-blur")); + })); + + this.container.on("click", selector, this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + if ($(e.target).closest(".select2-search-choice").length > 0) { + // clicked inside a select2 search choice, do not open + return; + } + this.selectChoice(null); + this.clearPlaceholder(); + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.open(); + this.focusSearch(); + e.preventDefault(); + })); + + this.container.on("focus", selector, this.bind(function () { + if (!this.isInterfaceEnabled()) return; + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + this.dropdown.addClass("select2-drop-active"); + this.clearPlaceholder(); + })); + + this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); + + // set the placeholder if necessary + this.clearSearch(); + }, + + // multi + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.search.prop("disabled", !this.isInterfaceEnabled()); + } + }, + + // multi + initSelection: function () { + var data; + if (this.opts.element.val() === "" && this.opts.element.text() === "") { + this.updateSelection([]); + this.close(); + // set the placeholder if necessary + this.clearSearch(); + } + if (this.select || this.opts.element.val() !== "") { + var self = this; + this.opts.initSelection.call(null, this.opts.element, function(data){ + if (data !== undefined && data !== null) { + self.updateSelection(data); + self.close(); + // set the placeholder if necessary + self.clearSearch(); + } + }); + } + }, + + // multi + clearSearch: function () { + var placeholder = this.getPlaceholder(), + maxWidth = this.getMaxSearchWidth(); + + if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { + this.search.val(placeholder).addClass("select2-default"); + // stretch the search box to full width of the container so as much of the placeholder is visible as possible + // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944 + this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); + } else { + this.search.val("").width(10); + } + }, + + // multi + clearPlaceholder: function () { + if (this.search.hasClass("select2-default")) { + this.search.val("").removeClass("select2-default"); + } + }, + + // multi + opening: function () { + this.clearPlaceholder(); // should be done before super so placeholder is not used to search + this.resizeSearch(); + + this.parent.opening.apply(this, arguments); + + this.focusSearch(); + + this.updateResults(true); + this.search.focus(); + this.opts.element.trigger($.Event("select2-open")); + }, + + // multi + close: function () { + if (!this.opened()) return; + this.parent.close.apply(this, arguments); + }, + + // multi + focus: function () { + this.close(); + this.search.focus(); + }, + + // multi + isFocused: function () { + return this.search.hasClass("select2-focused"); + }, + + // multi + updateSelection: function (data) { + var ids = [], filtered = [], self = this; + + // filter out duplicates + $(data).each(function () { + if (indexOf(self.id(this), ids) < 0) { + ids.push(self.id(this)); + filtered.push(this); + } + }); + data = filtered; + + this.selection.find(".select2-search-choice").remove(); + $(data).each(function () { + self.addSelectedChoice(this); + }); + self.postprocessResults(); + }, + + // multi + tokenize: function() { + var input = this.search.val(); + input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts); + if (input != null && input != undefined) { + this.search.val(input); + if (input.length > 0) { + this.open(); + } + } + + }, + + // multi + onSelect: function (data, options) { + + if (!this.triggerSelect(data)) { return; } + + this.addSelectedChoice(data); + + this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); + + if (this.select || !this.opts.closeOnSelect) this.postprocessResults(); + + if (this.opts.closeOnSelect) { + this.close(); + this.search.width(10); + } else { + if (this.countSelectableResults()>0) { + this.search.width(10); + this.resizeSearch(); + if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) { + // if we reached max selection size repaint the results so choices + // are replaced with the max selection reached message + this.updateResults(true); + } + this.positionDropdown(); + } else { + // if nothing left to select close + this.close(); + this.search.width(10); + } + } + + // since its not possible to select an element that has already been + // added we do not need to check if this is a new element before firing change + this.triggerChange({ added: data }); + + if (!options || !options.noFocus) + this.focusSearch(); + }, + + // multi + cancel: function () { + this.close(); + this.focusSearch(); + }, + + addSelectedChoice: function (data) { + var enableChoice = !data.locked, + enabledItem = $( + "
  • " + + "
    " + + " " + + "
  • "), + disabledItem = $( + "
  • " + + "
    " + + "
  • "); + var choice = enableChoice ? enabledItem : disabledItem, + id = this.id(data), + val = this.getVal(), + formatted, + cssClass; + + formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup); + if (formatted != undefined) { + choice.find("div").replaceWith("
    "+formatted+"
    "); + } + cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); + if (cssClass != undefined) { + choice.addClass(cssClass); + } + + if(enableChoice){ + choice.find(".select2-search-choice-close") + .on("mousedown", killEvent) + .on("click dblclick", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){ + this.unselect($(e.target)); + this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); + this.close(); + this.focusSearch(); + })).dequeue(); + killEvent(e); + })).on("focus", this.bind(function () { + if (!this.isInterfaceEnabled()) return; + this.container.addClass("select2-container-active"); + this.dropdown.addClass("select2-drop-active"); + })); + } + + choice.data("select2-data", data); + choice.insertBefore(this.searchContainer); + + val.push(id); + this.setVal(val); + }, + + // multi + unselect: function (selected) { + var val = this.getVal(), + data, + index; + + selected = selected.closest(".select2-search-choice"); + + if (selected.length === 0) { + throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; + } + + data = selected.data("select2-data"); + + if (!data) { + // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued + // and invoked on an element already removed + return; + } + + index = indexOf(this.id(data), val); + + if (index >= 0) { + val.splice(index, 1); + this.setVal(val); + if (this.select) this.postprocessResults(); + } + selected.remove(); + + this.opts.element.trigger({ type: "removed", val: this.id(data), choice: data }); + this.triggerChange({ removed: data }); + }, + + // multi + postprocessResults: function (data, initial, noHighlightUpdate) { + var val = this.getVal(), + choices = this.results.find(".select2-result"), + compound = this.results.find(".select2-result-with-children"), + self = this; + + choices.each2(function (i, choice) { + var id = self.id(choice.data("select2-data")); + if (indexOf(id, val) >= 0) { + choice.addClass("select2-selected"); + // mark all children of the selected parent as selected + choice.find(".select2-result-selectable").addClass("select2-selected"); + } + }); + + compound.each2(function(i, choice) { + // hide an optgroup if it doesnt have any selectable children + if (!choice.is('.select2-result-selectable') + && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) { + choice.addClass("select2-selected"); + } + }); + + if (this.highlight() == -1 && noHighlightUpdate !== false){ + self.highlight(0); + } + + //If all results are chosen render formatNoMAtches + if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ + if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { + if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { + this.results.append("
  • " + self.opts.formatNoMatches(self.search.val()) + "
  • "); + } + } + } + + }, + + // multi + getMaxSearchWidth: function() { + return this.selection.width() - getSideBorderPadding(this.search); + }, + + // multi + resizeSearch: function () { + var minimumWidth, left, maxWidth, containerLeft, searchWidth, + sideBorderPadding = getSideBorderPadding(this.search); + + minimumWidth = measureTextWidth(this.search) + 10; + + left = this.search.offset().left; + + maxWidth = this.selection.width(); + containerLeft = this.selection.offset().left; + + searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; + + if (searchWidth < minimumWidth) { + searchWidth = maxWidth - sideBorderPadding; + } + + if (searchWidth < 40) { + searchWidth = maxWidth - sideBorderPadding; + } + + if (searchWidth <= 0) { + searchWidth = minimumWidth; + } + + this.search.width(searchWidth); + }, + + // multi + getVal: function () { + var val; + if (this.select) { + val = this.select.val(); + return val === null ? [] : val; + } else { + val = this.opts.element.val(); + return splitVal(val, this.opts.separator); + } + }, + + // multi + setVal: function (val) { + var unique; + if (this.select) { + this.select.val(val); + } else { + unique = []; + // filter out duplicates + $(val).each(function () { + if (indexOf(this, unique) < 0) unique.push(this); + }); + this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); + } + }, + + // multi + buildChangeDetails: function (old, current) { + var current = current.slice(0), + old = old.slice(0); + + // remove intersection from each array + for (var i = 0; i < current.length; i++) { + for (var j = 0; j < old.length; j++) { + if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { + current.splice(i, 1); + i--; + old.splice(j, 1); + j--; + } + } + } + + return {added: current, removed: old}; + }, + + + // multi + val: function (val, triggerChange) { + var oldData, self=this, changeDetails; + + if (arguments.length === 0) { + return this.getVal(); + } + + oldData=this.data(); + if (!oldData.length) oldData=[]; + + // val is an id. !val is true for [undefined,null,'',0] - 0 is legal + if (!val && val !== 0) { + this.opts.element.val(""); + this.updateSelection([]); + this.clearSearch(); + if (triggerChange) { + this.triggerChange({added: this.data(), removed: oldData}); + } + return; + } + + // val is a list of ids + this.setVal(val); + + if (this.select) { + this.opts.initSelection(this.select, this.bind(this.updateSelection)); + if (triggerChange) { + this.triggerChange(this.buildChangeDetails(oldData, this.data())); + } + } else { + if (this.opts.initSelection === undefined) { + throw new Error("val() cannot be called if initSelection() is not defined"); + } + + this.opts.initSelection(this.opts.element, function(data){ + var ids=$.map(data, self.id); + self.setVal(ids); + self.updateSelection(data); + self.clearSearch(); + if (triggerChange) { + self.triggerChange(this.buildChangeDetails(oldData, this.data())); + } + }); + } + this.clearSearch(); + }, + + // multi + onSortStart: function() { + if (this.select) { + throw new Error("Sorting of elements is not supported when attached to instead."); + } + + // collapse search field into 0 width so its container can be collapsed as well + this.search.width(0); + // hide the container + this.searchContainer.hide(); + }, + + // multi + onSortEnd:function() { + + var val=[], self=this; + + // show search and move it to the end of the list + this.searchContainer.show(); + // make sure the search container is the last item in the list + this.searchContainer.appendTo(this.searchContainer.parent()); + // since we collapsed the width in dragStarted, we resize it here + this.resizeSearch(); + + // update selection + this.selection.find(".select2-search-choice").each(function() { + val.push(self.opts.id($(this).data("select2-data"))); + }); + this.setVal(val); + this.triggerChange(); + }, + + // multi + data: function(values, triggerChange) { + var self=this, ids, old; + if (arguments.length === 0) { + return this.selection + .find(".select2-search-choice") + .map(function() { return $(this).data("select2-data"); }) + .get(); + } else { + old = this.data(); + if (!values) { values = []; } + ids = $.map(values, function(e) { return self.opts.id(e); }); + this.setVal(ids); + this.updateSelection(values); + this.clearSearch(); + if (triggerChange) { + this.triggerChange(this.buildChangeDetails(old, this.data())); + } + } + } + }); + + $.fn.select2 = function () { + + var args = Array.prototype.slice.call(arguments, 0), + opts, + select2, + method, value, multiple, + allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data", "search"], + valueMethods = ["val", "opened", "isFocused", "container", "data"], + methodsMap = { search: "externalSearch" }; + + this.each(function () { + if (args.length === 0 || typeof(args[0]) === "object") { + opts = args.length === 0 ? {} : $.extend({}, args[0]); + opts.element = $(this); + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + multiple = opts.element.prop("multiple"); + } else { + multiple = opts.multiple || false; + if ("tags" in opts) {opts.multiple = multiple = true;} + } + + select2 = multiple ? new MultiSelect2() : new SingleSelect2(); + select2.init(opts); + } else if (typeof(args[0]) === "string") { + + if (indexOf(args[0], allowedMethods) < 0) { + throw "Unknown method: " + args[0]; + } + + value = undefined; + select2 = $(this).data("select2"); + if (select2 === undefined) return; + + method=args[0]; + + if (method === "container") { + value = select2.container; + } else if (method === "dropdown") { + value = select2.dropdown; + } else { + if (methodsMap[method]) method = methodsMap[method]; + + value = select2[method].apply(select2, args.slice(1)); + } + if (indexOf(args[0], valueMethods) >= 0) { + return false; + } + } else { + throw "Invalid arguments to select2 plugin: " + args; + } + }); + return (value === undefined) ? this : value; + }; + + // plugin defaults, accessible to users + $.fn.select2.defaults = { + width: "copy", + loadMorePadding: 0, + closeOnSelect: true, + openOnEnter: true, + containerCss: {}, + dropdownCss: {}, + containerCssClass: "", + dropdownCssClass: "", + formatResult: function(result, container, query, escapeMarkup) { + var markup=[]; + markMatch(result.text, query.term, markup, escapeMarkup); + return markup.join(""); + }, + formatSelection: function (data, container, escapeMarkup) { + return data ? escapeMarkup(data.text) : undefined; + }, + sortResults: function (results, container, query) { + return results; + }, + formatResultCssClass: function(data) {return undefined;}, + formatSelectionCssClass: function(data, container) {return undefined;}, + formatNoMatches: function () { return "No matches found"; }, + formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); }, + formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, + formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, + formatLoadMore: function (pageNumber) { return "Loading more results..."; }, + formatSearching: function () { return "Searching..."; }, + minimumResultsForSearch: 0, + minimumInputLength: 0, + maximumInputLength: null, + maximumSelectionSize: 0, + id: function (e) { return e.id; }, + matcher: function(term, text) { + return (''+text).toUpperCase().indexOf((''+term).toUpperCase()) >= 0; + }, + separator: ",", + tokenSeparators: [], + tokenizer: defaultTokenizer, + escapeMarkup: defaultEscapeMarkup, + blurOnChange: false, + selectOnBlur: false, + adaptContainerCssClass: function(c) { return c; }, + adaptDropdownCssClass: function(c) { return null; } + }; + + $.fn.select2.ajaxDefaults = { + transport: $.ajax, + params: { + type: "GET", + cache: false, + dataType: "json" + } + }; + + // exports + window.Select2 = { + query: { + ajax: ajax, + local: local, + tags: tags + }, util: { + debounce: debounce, + markMatch: markMatch, + escapeMarkup: defaultEscapeMarkup + }, "class": { + "abstract": AbstractSelect2, + "single": SingleSelect2, + "multi": MultiSelect2 + } + }; + +}(jQuery)); From d75be4d8a2b44fd9317fa778d69fdfaac8d580e6 Mon Sep 17 00:00:00 2001 From: Hryhorii Hrebiniuk Date: Wed, 31 Jul 2013 19:14:43 +0300 Subject: [PATCH 029/541] Implemented BAP-1289 & BAP-1290: Remember selection on pagination; "All" and "All Visible" --- .../GridBundle/Resources/config/assets.yml | 1 + .../js/app/datagrid/cell/selectrowcell.js | 267 ++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js diff --git a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml index dce45537148..57b2dde8af9 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml @@ -26,6 +26,7 @@ js: - '@OroGridBundle/Resources/public/js/app/datagrid/cell/stringcell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/cell/htmlcell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/cell/selectcell.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/headercell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/header.js' diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js new file mode 100644 index 00000000000..c5642b3e949 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js @@ -0,0 +1,267 @@ +/* jshint browser:true */ +/* global define, Oro, $, _, Backbone, Backgrid */ +(function (factory) { + "use strict"; + if (typeof define === 'function' && define.amd) { + define(['Oro', 'jQuery', '_', 'Backbone', 'Backgrid'], factory); + } else { + factory(); + } +}(function () { + "use strict"; + Oro.Datagrid = Oro.Datagrid || {}; + Oro.Datagrid.Cell = Oro.Datagrid.Cell || {}; + + /** + * Renders a checkbox for row selection. + * + * @class Oro.Datagrid.Cell.SelectRowCell + * @extends Backbone.View + */ + Oro.Datagrid.Cell.SelectRowCell = Backbone.View.extend({ + + /** @property */ + className: "select-row-cell", + + /** @property */ + tagName: "td", + + /** @property */ + events: { + "change :checkbox": "onChange", + "click": "enterEditMode" + }, + + /** + * Initializer. If the underlying model triggers a `select` event, this cell + * will change its checked value according to the event's `selected` value. + * + * @param {Object} options + * @param {Backgrid.Column} options.column + * @param {Backbone.Model} options.model + */ + initialize: function (options) { + //Backgrid.requireOptions(options, ["model", "column"]); + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + this.listenTo(this.model, "backgrid:select", function (model, checked) { + this.$el.find(":checkbox").prop("checked", checked).change(); + }); + + }, + + /** + * Focuses the checkbox. + */ + enterEditMode: function (e) { + var $checkbox = this.$el.find(":checkbox").focus(); + if ($checkbox[0] !== e.target) { + $checkbox.prop("checked", !$checkbox.prop("checked")).change(); + } + e.stopPropagation(); + }, + + /** + * Unfocuses the checkbox. + */ + exitEditMode: function () { + this.$el.find(":checkbox").blur(); + }, + + /** + * When the checkbox's value changes, this method will trigger a Backbone + * `backgrid:selected` event with a reference of the model and the + * checkbox's `checked` value. + */ + onChange: function (e) { + this.model.trigger("backgrid:selected", this.model, $(e.target).prop("checked")); + }, + + /** + * Renders a checkbox in a table cell. + */ + render: function () { + // work around with trigger event to get current state of model (selected or not) + var state = {selected: false}; + this.$el.empty().append(''); + this.model.trigger('backgrid:isSelected', this.model, state); + if (state.selected) { + this.$el.find(':checkbox').prop('checked', 'checked'); + } + this.delegateEvents(); + return this; + } + }); + + /** + * Contains mass-selection logic + * - watches models selection, keeps reference to selected + * - provides mass-selection actions + * - listening to models collection events, + * fills in 'obj' with proper data for + * `backgrid:isSelected` and `backgrid:getSelected` + * + * @class Oro.Datagrid.Cell.SelectAllHeaderCell + * @extends Oro.Datagrid.Cell.SelectRowCell + */ + Oro.Datagrid.Cell.SelectAllHeaderCell = Oro.Datagrid.Cell.SelectRowCell.extend({ + /** @property */ + className: "select-all-header-cell", + + /** @property */ + tagName: "th", + + events: {}, + + /** + * Initializer. + * Subscribers on events listening + * + * @param {Object} options + * @param {Backgrid.Column} options.column + * @param {Backbone.Collection} options.collection + */ + initialize: function (options) { + //Backgrid.requireOptions(options, ["column", "collection"]); + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + this.initialState(); + this.listenTo(this.collection, { + remove: this.removeModel, + updateState: this.initialState, + 'backgrid:selected': this.selectModel, + 'backgrid:selectAll': this.selectAll, + 'backgrid:selectAllVisible': this.selectAllVisible, + + 'backgrid:isSelected': _.bind(function (model, obj) { + if ($.isPlainObject(obj)) { + obj.selected = this.isSelectedModel(model); + } + }, this), + 'backgrid:getSelected': _.bind(function (obj) { + if ($.isEmptyObject(obj)) { + obj.selected = _.keys(this.selectedModels); + obj.inset = this.inset; + } + }, this) + }); + }, + + /** + * Resets selection to initial conditions + * - clear selected models set + * - reset set type in-set/not-in-set + */ + initialState: function () { + this.selectedModels = {}; + this.inset = true; + }, + + /** + * Checks if passed model have to be marked as selected + * + * @param {Backbone.Model} model + * @returns {boolean} + */ + isSelectedModel: function (model) { + return this.inset === _.has(this.selectedModels, model.id || model.cid); + }, + + /** + * Removes model from selected models set + * + * @param {Backbone.Model} model + */ + removeModel: function (model) { + delete this.selectedModels[model.id || model.cid]; + }, + + /** + * Adds/removes model to/from selected models set + * + * @param {Backbone.Model} model + * @param {boolean} selected + */ + selectModel: function (model, selected) { + if (selected === this.inset) { + this.selectedModels[model.id || model.cid] = model; + } else { + this.removeModel(model); + } + }, + + /** + * Performs selection of all possible models: + * - reset to initial state + * - change type of set type as not-inset + * - marks all models in collection as selected + * start to collect models which have to be excluded + */ + selectAll: function () { + this.initialState(); + this.inset = false; + this._selectAll(); + }, + + /** + * Performs selection of all visible models: + * - if necessary reset to initial state + * - marks all models in collection as selected + */ + selectAllVisible: function () { + if (!this.inset) { + this.initialState(); + } + this._selectAll(); + }, + + /** + * Marks all models in collection as selected + * + * @private + */ + _selectAll: function () { + this.collection.each(function (model) { + model.trigger("backgrid:select", model, true); + }); + }, + + /** + * + * + * @returns {Oro.Datagrid.Cell.SelectAllHeaderCell} + */ + render: function () { + /*jshint multistr:true */ + /*jslint es5: true */ + /* render method will detend on options or will be empty */ + this.$el.empty().append('
    \ + \ + \ + \ +
    '); + this.$el.find('[data-select-all]').on('click', _.bind(function (e) { + this.collection.trigger('backgrid:selectAll'); + e.preventDefault(); + }, this)); + this.$el.find('[data-select-all-visible]').on('click', _.bind(function (e) { + this.collection.trigger('backgrid:selectAllVisible'); + e.preventDefault(); + }, this)); + /* */ + return this; + } + }); +})); From 344eb8dd7ace30a1784816fe153cf8d364688bec Mon Sep 17 00:00:00 2001 From: Vova Soroka Date: Wed, 31 Jul 2013 20:17:13 +0300 Subject: [PATCH 030/541] CRM-286: Correspondence visibility -CRM-294: Create doctrine entity classes in EmailBundle -CRM-301: Add TWIG template which is used to display a particular email details -CRM-298: Add template to display emails --- .../EmailBundle/Cache/EmailCacheManager.php | 99 ++++++++++++++ .../Controller/EmailController.php | 100 ++++++++++++++ src/Oro/Bundle/EmailBundle/Entity/Email.php | 25 ++-- .../EmailBundle/Entity/EmailAddress.php | 7 +- .../EmailBundle/Entity/EmailAttachment.php | 42 +++++- .../Entity/EmailAttachmentContent.php | 124 ++++++++++++++++++ .../Bundle/EmailBundle/Entity/EmailBody.php | 24 ++-- .../Bundle/EmailBundle/Entity/EmailFolder.php | 3 + .../EmailBundle/Entity/EmailRecipient.php | 6 + .../Entity/Repository/EmailRepository.php | 56 ++++++++ .../EmailBundle/Resources/config/assets.yml | 1 + .../EmailBundle/Resources/config/services.yml | 7 + .../Resources/public/js/activity.js | 17 +++ .../views/Email/activities.html.twig | 26 ++++ .../Resources/views/Email/activity.html.twig | 7 + .../Resources/views/Email/view.html.twig | 76 +++++++++++ .../Resources/public/css/less/oro.less | 23 +++- .../Resources/public/js/views/dialog.js | 30 +++++ 18 files changed, 648 insertions(+), 25 deletions(-) create mode 100644 src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php create mode 100644 src/Oro/Bundle/EmailBundle/Controller/EmailController.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php create mode 100644 src/Oro/Bundle/EmailBundle/Resources/public/js/activity.js create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig diff --git a/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php b/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php new file mode 100644 index 00000000000..4971b7c609b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php @@ -0,0 +1,99 @@ +em = $em; + } + + /** + * Check that email body is cached. If not load it through an email service connector and add it to a cache + * + * @param Email $email + */ + public function ensureEmailBodyCached(Email $email) + { + if ($email->getEmailBody() !== null) { + // The email body is already cached + return; + } + + // TODO: implement getting email details through correct connector here + + //$emailOriginName = $email->getFolder()->getOrigin()->getName(); + //$connector = $this->get(sprintf('oro_%s.connector', $emailOriginName)); + + $emailBody = new EmailBody(); + $emailBody + ->setHeader($email) + ->setContent("\n

    Sample Email Body

    \n some text \n some text \n some text \n some text \n some text"); + + $emailBody->addAttachment( + $this->createEmailAttachment( + 'sample attachment file.txt', + 'text/plain', + 'binary', + 'some text' + ) + ); + + $emailBody->addAttachment( + $this->createEmailAttachment( + 'sample attachment file (base64).txt', + 'text/plain', + 'base64', + 'some text' + ) + ); + + $email->setEmailBody($emailBody); + + $this->em->persist($email); + $this->em->flush(); + } + + /** + * Create CreateEmailAttachment object + * + * @param string $fileName + * @param string $contentType + * @param string $contentTransferEncoding + * @param string $content + * @return EmailAttachment + */ + protected function createEmailAttachment($fileName, $contentType, $contentTransferEncoding, $content) + { + $emailAttachmentContent = new EmailAttachmentContent(); + $emailAttachmentContent + ->setContentTransferEncoding($contentTransferEncoding) + ->setValue($content); + + $emailAttachment = new EmailAttachment(); + $emailAttachment + ->setFileName($fileName) + ->setContentType($contentType) + ->setContent($emailAttachmentContent); + + return $emailAttachment; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/EmailController.php b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php new file mode 100644 index 00000000000..45574000ce7 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php @@ -0,0 +1,100 @@ +get('oro_email.cache.manager'); + $emailCacheManager->ensureEmailBodyCached($entity); + + return array( + 'entity' => $entity + ); + } + + /** + * Get email list + * TODO: This is a temporary action created for demo purposes. It will be removed when 'display activities' functionality is implemented + * + * @AclAncestor("oro_email_view") + * @Template + */ + public function activitiesAction(ArrayCollection $emails) + { + /** @var $emailRepository EmailRepository */ + $emailRepository = $this->getDoctrine()->getRepository('OroEmailBundle:Email'); + $qb = $emailRepository->getEmailListQueryBuilder($emails); + + $rows = $qb->getQuery()->execute(); + + return array( + 'entities' => $rows + ); + } + + /** + * Get the given email body content + * + * @Route("/body/{id}", name="oro_email_body", requirements={"id"="\d+"}) + * @AclAncestor("oro_email_view") + */ + public function bodyAction(EmailBody $entity) + { + return new Response($entity->getContent()); + } + + /** + * Get a response for download the given email attachment + * + * @Route("/attachment/{id}", name="oro_email_attachment", requirements={"id"="\d+"}) + * @AclAncestor("oro_email_view") + */ + public function attachmentAction(EmailAttachment $entity) + { + $response = new Response(); + $response->headers->set('Content-Type', $entity->getContentType()); + $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $entity->getFileName())); + $response->headers->set('Content-Transfer-Encoding', $entity->getContent()->getContentTransferEncoding()); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', '0'); + $response->setContent($entity->getContent()->getValue()); + + return $response; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Email.php b/src/Oro/Bundle/EmailBundle/Entity/Email.php index 5774f43750c..013c2c8ff44 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Email.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Email.php @@ -11,6 +11,7 @@ * * @ORM\Table(name="oro_email") * @ORM\Entity + * @ORM\Entity(repositoryClass="Oro\Bundle\EmailBundle\Entity\Repository\EmailRepository") * @ORM\HasLifecycleCallbacks */ class Email @@ -70,14 +71,14 @@ class Email * * @ORM\Column(name="received", type="datetime") */ - protected $received; + protected $receivedAt; /** * @var \DateTime * * @ORM\Column(name="sent", type="datetime") */ - protected $sent; + protected $sentAt; /** * @var integer @@ -132,6 +133,7 @@ class Email public function __construct() { + $this->importance = self::NORMAL_IMPORTANCE; $this->recipients = new ArrayCollection(); } @@ -266,18 +268,18 @@ public function addRecipient(EmailRecipient $recipient) */ public function getReceivedAt() { - return $this->received; + return $this->receivedAt; } /** * Set date/time when email received * - * @param \DateTime $received + * @param \DateTime $receivedAt * @return $this */ - public function setReceivedAt($received) + public function setReceivedAt($receivedAt) { - $this->received = $received; + $this->receivedAt = $receivedAt; return $this; } @@ -289,18 +291,18 @@ public function setReceivedAt($received) */ public function getSentAt() { - return $this->sent; + return $this->sentAt; } /** * Set date/time when email sent * - * @param \DateTime $sent + * @param \DateTime $sentAt * @return $this */ - public function setSentAt($sent) + public function setSentAt($sentAt) { - $this->sent = $sent; + $this->sentAt = $sentAt; return $this; } @@ -473,9 +475,6 @@ public function setEmailBody(EmailBody $emailBody) */ public function beforeSave() { - if (!isset($this->importance)) { - $this->importance = self::NORMAL_IMPORTANCE; - } $this->created = new \DateTime('now', new \DateTimeZone('UTC')); } } diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php index a49e0e0b4e3..e708ed128a3 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php @@ -7,7 +7,9 @@ /** * Email Address * - * @ORM\Table(name="oro_email_address") + * @ORM\Table(name="oro_email_address", + * uniqueConstraints={@ORM\UniqueConstraint(name="oro_email_address_uq", columns={"email_address"})}, + * indexes={@ORM\Index(name="oro_email_address_idx", columns={"email_address"})}) * @ORM\Entity */ class EmailAddress @@ -54,9 +56,12 @@ public function getEmailAddress() * It means that if the full email is "John Smith" the email address is john@example.com * * @param string $emailAddress + * @return $this */ public function setEmailAddress($emailAddress) { $this->emailAddress = $emailAddress; + + return $this; } } diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php index aec8136e2aa..027bc116501 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\EmailBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation\Exclude; /** * Email Attachment @@ -35,10 +36,18 @@ class EmailAttachment */ protected $contentType; + /** + * @var EmailAttachmentContent + * + * @ORM\OneToOne(targetEntity="EmailAttachmentContent", mappedBy="emailAttachment", cascade={"persist", "remove"}, orphanRemoval=true) + * @Exclude + */ + protected $attachmentContent; + /** * @var EmailBody * - * @ORM\ManyToOne(targetEntity="EmailBody", inversedBy="folders") + * @ORM\ManyToOne(targetEntity="EmailBody", inversedBy="attachments") * @ORM\JoinColumn(name="body_id", referencedColumnName="id") */ protected $emailBody; @@ -67,10 +76,13 @@ public function getFileName() * Set attachment file name * * @param string $fileName + * @return $this */ public function setFileName($fileName) { $this->fileName = $fileName; + + return $this; } /** @@ -87,10 +99,38 @@ public function getContentType() * Set content type * * @param string $contentType any MIME type + * @return $this */ public function setContentType($contentType) { $this->contentType = $contentType; + + return $this; + } + + /** + * Get content of email attachment + * + * @return EmailAttachmentContent + */ + public function getContent() + { + return $this->attachmentContent; + } + + /** + * Set content of email attachment + * + * @param EmailAttachmentContent $attachmentContent + * @return $this + */ + public function setContent(EmailAttachmentContent $attachmentContent) + { + $this->attachmentContent = $attachmentContent; + + $attachmentContent->setEmailAttachment($this); + + return $this; } /** diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php new file mode 100644 index 00000000000..5833468f45e --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php @@ -0,0 +1,124 @@ +id; + } + + /** + * Get email attachment owner + * + * @return EmailAttachment + */ + public function getEmailAttachment() + { + return $this->emailAttachment; + } + + /** + * Set email attachment owner + * + * @param EmailAttachment $emailAttachment + * @return $this + */ + public function setEmailAttachment(EmailAttachment $emailAttachment) + { + $this->emailAttachment = $emailAttachment; + + return $this; + } + + /** + * Get attachment content + * + * @return string + */ + public function getValue() + { + return $this->content; + } + + /** + * Set attachment content + * + * @param string $content + * @return $this + */ + public function setValue($content) + { + $this->content = $content; + + return $this; + } + + /** + * Get encoding type of attachment content + * + * @return string + */ + public function getContentTransferEncoding() + { + return $this->contentTransferEncoding; + } + + /** + * Set encoding type of attachment content + * + * @param string $contentTransferEncoding + * @return $this + */ + public function setContentTransferEncoding($contentTransferEncoding) + { + $this->contentTransferEncoding = $contentTransferEncoding; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php index 0f182ab307c..b0a39055c18 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php @@ -77,6 +77,9 @@ class EmailBody public function __construct() { + $this->bodyIsText = false; + $this->hasAttachments = false; + $this->persistent = false; $this->attachments = new ArrayCollection(); } @@ -114,10 +117,13 @@ public function getContent() * Set body content. * * @param string $bodyContent + * @return $this */ public function setContent($bodyContent) { $this->bodyContent = $bodyContent; + + return $this; } /** @@ -134,10 +140,13 @@ public function getBodyIsText() * Set body content type. * * @param bool $bodyIsText true for text/plain, false for text/html + * @return $this */ public function setBodyIsText($bodyIsText) { $this->bodyIsText = $bodyIsText; + + return $this; } /** @@ -154,10 +163,13 @@ public function getHasAttachments() * Set flag indicates whether there are attachments or not. * * @param bool $hasAttachments + * @return $this */ public function setHasAttachments($hasAttachments) { $this->hasAttachments = $hasAttachments; + + return $this; } /** @@ -174,10 +186,13 @@ public function getPersistent() * Set flag indicates whether email can be removed from the cache or not. * * @param bool $persistent true if this email newer removed from the cache; otherwise, false + * @return $this */ public function setPersistent($persistent) { $this->persistent = $persistent; + + return $this; } /** @@ -237,15 +252,6 @@ public function addAttachment(EmailAttachment $attachment) */ public function beforeSave() { - if (!isset($this->bodyIsText)) { - $this->bodyIsText = false; - } - if (!isset($this->hasAttachments)) { - $this->hasAttachments = false; - } - if (!isset($this->persistent)) { - $this->persistent = false; - } $this->created = new \DateTime('now', new \DateTimeZone('UTC')); } } diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php index efe28e78c69..bf5ec9ba5cf 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php @@ -111,10 +111,13 @@ public function getType() * Set folder type * * @param string $type Can be 'inbox', 'sent', 'trash', 'drafts' or 'other' + * @return $this */ public function setType($type) { $this->type = $type; + + return $this; } /** diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php b/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php index 38cffc16571..29add21c81e 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailRecipient.php @@ -79,10 +79,13 @@ public function getName() * Set full email name * * @param string $name + * @return $this */ public function setName($name) { $this->name = $name; + + return $this; } /** @@ -99,10 +102,13 @@ public function getType() * Set recipient type * * @param string $type Can be 'to', 'cc' or 'bcc' + * @return $this */ public function setType($type) { $this->type = $type; + + return $this; } /** diff --git a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php new file mode 100644 index 00000000000..4547168bfc0 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php @@ -0,0 +1,56 @@ +getEntityManager()->createQueryBuilder() + ->select('e1.id') + ->from('OroEmailBundle:Email', 'e1') + ->innerJoin('e1.recipients', 'r') + ->innerJoin('r.emailAddress', 'a'); + $emailAddresses = array(); + if ($recipients instanceof \Traversable) { + foreach ($recipients as $recipient) { + $emailAddresses[] = EmailUtil::extractPureEmailAddress($recipient); + } + } else { + $emailAddresses[] = EmailUtil::extractPureEmailAddress($recipients); + } + $qbRecipients->where($qbRecipients->expr()->in('a.emailAddress', $emailAddresses)); + + $qb = $this->createQueryBuilder('e') + ->select( + 'e.id', + 'e.fromName', + 'e.subject', + 'e.sentAt' + ) + ->orderBy('e.created', 'DESC'); + $qb->where($qb->expr()->in('e.id', $qbRecipients->getDQL())); + + if ($firstResult !== null) { + $qb->setFirstResult($firstResult); + } + if ($maxResults !== null) { + $qb->setMaxResults($maxResults); + } + + return $qb; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml index 412124533f8..51f2ef4878c 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml @@ -3,6 +3,7 @@ js: - '@OroEmailBundle/Resources/public/js/views/templates.updater.js' - '@OroEmailBundle/Resources/public/js/models/templates.updater.js' - '@OroEmailBundle/Resources/public/js/collections/templates.updater.js' + - '@OroEmailBundle/Resources/public/js/activity.js' css: 'email': - '@OroEmailBundle/Resources/public/css/style.css' diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index b4dc39d581d..78a10016b43 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -1,4 +1,6 @@ parameters: + oro_email.cache.manager.class: Oro\Bundle\EmailBundle\Cache\EmailCacheManager + oro_email.emailtemplate.entity.class: Oro\Bundle\EmailBundle\Entity\EmailTemplate # Email template field @@ -15,6 +17,11 @@ parameters: oro_email.form.type.emailtemplate.api.class: Oro\Bundle\EmailBundle\Form\Type\EmailTemplateApiType services: + oro_email.cache.manager: + class: %oro_email.cache.manager.class% + arguments: + - @doctrine.orm.entity_manager + # Email template field oro_email.form.subscriber.emailtemplate: class: %oro_email.form.subscriber.emailtemplate.class% diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/activity.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/activity.js new file mode 100644 index 00000000000..79e1e6e9f6e --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/activity.js @@ -0,0 +1,17 @@ +$(function() { + $(document).on('click', '.view-email-body-btn', function (e) { + new Oro.widget.DialogView({ + url: $(this).attr('href'), + dialogOptions: { + allowMaximize: true, + allowMinimize: true, + dblclick: 'maximize', + maximizedHeightDecreaseBy: 'minimize-bar', + width : 1000, + title: $(this).attr('title') + } + }).render(); + + return false; + }); +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig new file mode 100644 index 00000000000..f7aec13118d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig @@ -0,0 +1,26 @@ +{# TODO: This is a temporary template created for demo purposes. It will be removed when 'display activities' functionality is implemented #} +
    +
    +
    +
    +
    + + + + + + + + + + + {% for entity in entities %} + {% include 'OroEmailBundle:Email:activity.html.twig' with {'entity': entity} %} + {% endfor %} + +
    IDFrom Subject Sent
    +
    +
    +
    +
    +
    diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig new file mode 100644 index 00000000000..19bf33416f0 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig @@ -0,0 +1,7 @@ +{# TODO: This is a demo template. It need to be replaced with a real one #} + + {{ entity.id }} + {{ entity.fromName }} + {{ entity.subject }} + {{ entity.sentAt|date('m/d/Y H:m:s') }} + diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig new file mode 100644 index 00000000000..9a469f52a3d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig @@ -0,0 +1,76 @@ +{% block page_container %} + +{% endblock %} + +{% macro renderRow(title, iClass, value) %} + {% macro row_data(value) %} +
    + {% if value is iterable %} + {% for val in value %} + {% if val.name is not defined %} +

    {{ val }}

    + {% else %} +

    {{ val.name }}

    + {% endif %} + {% if not loop.last %} + , + {% endif %} + {% endfor %} + {% else %} +

    {{ value }}

    + {% endif %} +
    + {% endmacro %} +
    + + +
    + {{ _self.row_data(value)|raw }} +
    +
    +{% endmacro %} + +{% macro renderAttachmentsRow(title, value) %} + {% macro attachments_row_data(value) %} +
    + +
    + {% endmacro %} + +{% endmacro %} diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index 23ba328516c..474dda6ae71 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -4133,4 +4133,25 @@ button.btn.minimize-button i.icon-minimize-active{ #pin-button-div{ float:right; -} \ No newline at end of file +} + +.email-body-holder pre.email-body { + width: 99%; + min-height: 5em; +} +.email-body-holder iframe.email-body { + width: 99%; + min-height: 5em; + height: 25em; + #font > #family > .monospace; + color: @grayDark; + .border-radius(3px); + padding: (@baseLineHeight - 1) / 2; + margin: 0 0 @baseLineHeight / 2; + font-size: @baseFontSize - 1; // 14px to 13px + line-height: @baseLineHeight; + background-color: #f5f5f5; + border: 1px solid #ccc; // fallback for IE7-8 + border: 1px solid rgba(0,0,0,.15); + .border-radius(@baseBorderRadius); +} diff --git a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js index b201b9812f0..99e5e68136e 100644 --- a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js +++ b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js @@ -11,6 +11,7 @@ Oro.widget.DialogView = Backbone.View.extend({ }, actions: null, firstRun: true, + contentTop: null, // Windows manager global variables windowsPerRow: 10, @@ -20,6 +21,7 @@ Oro.widget.DialogView = Backbone.View.extend({ windowY: 0, defaultPos: 'center center', openedWindows: 0, + contentTop: null, /** * Initialize dialog @@ -233,6 +235,34 @@ Oro.widget.DialogView = Backbone.View.extend({ } this.adoptActions(); + this.adjustHeight(); + }, + + adjustHeight: function() { + var content = this.widget.find('.scrollable-container'); + + // first execute + if (_.isNull(this.contentTop)) { + content.css('overflow', 'auto'); + + var parentEl = content.parent(); + var topPaddingOffset = parentEl.is(this.widget)?0:parentEl.position().top; + this.contentTop = content.position().top + topPaddingOffset; + var widgetHeight = this.widget.height(); + content.outerHeight(this.widget.height() - this.contentTop); + if (widgetHeight != this.widget.height()) { + // there is some unpredictable offset + this.contentTop += this.widget.height() - this.contentTop - content.outerHeight(); + content.outerHeight(this.widget.height() - this.contentTop); + } + this.widget.on("dialogresize", _.bind(this.adjustHeight, this)); + + } + + content.each(_.bind(function(i, el){ + var $el = $(el); + $el.outerHeight(this.widget.height() - this.contentTop); + },this)); }, /** From 5dcf56c2401a49dd257c97121a845c94d450d45d Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Wed, 31 Jul 2013 19:26:20 +0200 Subject: [PATCH 031/541] BAP-1233: Tags refactoring --- .../Form/Type/TagAutocompleteType.php | 46 +++++++++++++++++ .../TagBundle/Form/Type/TagSelectType.php | 30 ++++------- .../Bundle/TagBundle/Resources/config/acl.yml | 5 -- .../TagBundle/Resources/config/services.yml | 6 +++ .../Resources/views/Form/fields.html.twig | 50 ++++++++++++++++++- .../Resources/views/macros.html.twig | 31 ------------ .../Resources/views/User/update.html.twig | 3 +- 7 files changed, 112 insertions(+), 59 deletions(-) create mode 100644 src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php new file mode 100644 index 00000000000..924d56e8673 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php @@ -0,0 +1,46 @@ +setDefaults( + array( + 'configs' => array( + 'placeholder' => 'oro.tag.form.choose_tag', + 'extra_config' => 'multi_autocomplete', + 'multiple' => true + ), + 'autocomplete_alias' => 'tags', + 'inherit_data' => true + ) + ); + + $resolver->setNormalizers(array()); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_tag_autocomplete'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_jqueryselect2_hidden'; + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php index 0dbd89fa241..f9b22f5ffb0 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php @@ -3,11 +3,8 @@ use Doctrine\Common\Persistence\ObjectManager; use Oro\Bundle\TagBundle\Entity\TagManager; -use Oro\Bundle\TagBundle\Form\TagsTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TagSelectType extends AbstractType @@ -32,18 +29,16 @@ public function __construct(ObjectManager $om, TagManager $tagManager) $this->tagManager = $tagManager; } + /** + * {@inheritdoc} + */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( array( - 'configs' => array( - 'placeholder' => 'oro.tag.form.choose_tag', - 'multiple' => true, - 'tokenSeparators' => array(',', ' '), - 'tags' => true, - 'extra_config' => 'multi_autocomplete', - ), - 'autocomplete_alias' => 'tags', + 'fields' => array(), // ? + 'form' => array(), // ? + 'inherit_data' => true, ) ); } @@ -53,15 +48,10 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) */ public function buildForm(FormBuilderInterface $builder, array $options) { - $transformer = new TagsTransformer($this->om, 'Oro\Bundle\TagBundle\Entity\Tag'); - $transformer->setTagManager($this->tagManager); - - $builder->addModelTransformer($transformer); - } - - public function getParent() - { - return 'oro_jqueryselect2_hidden'; + $builder->add( + 'autocomplete', + 'oro_tag_autocomplete' + ); } /** diff --git a/src/Oro/Bundle/TagBundle/Resources/config/acl.yml b/src/Oro/Bundle/TagBundle/Resources/config/acl.yml index 4468a1918bf..1e0452f4b43 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/acl.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/acl.yml @@ -7,8 +7,3 @@ oro_tag_unassign_global: name: Tag unassign global description: User can unassign all tags from entities parent: oro_tag - -oro_tag_view_tag_cloud: - name: View tag cloud - description: User can view tag cloud - parent: oro_tag diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index 25a9fc4770c..05245887966 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -13,6 +13,7 @@ parameters: oro_tag.autocomplete.tag.search_handler.class: Oro\Bundle\FormBundle\Autocomplete\SearchHandler oro_tag.form.type.tag_select.class: Oro\Bundle\TagBundle\Form\Type\TagSelectType + oro_tag.form.type.tag_autocomplete.class: Oro\Bundle\TagBundle\Form\Type\TagAutocompleteType oro_tag.provider.search_provider.class: Oro\Bundle\TagBundle\Provider\SearchProvider @@ -106,6 +107,11 @@ services: - { name: oro_form.autocomplete.search_handler, alias: tags, acl_resource: oro_tag_assign_unassign } # Autocomplete form type + oro_tag.form.type.tag_autocomplete: + class: %oro_tag.form.type.tag_autocomplete.class% + tags: + - { name: form.type, alias: oro_tag_autocomplete } + oro_tag.form.type.tag_select: class: %oro_tag.form.type.tag_select.class% arguments: diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index 27735bda400..7fc4dd2ef04 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -1,3 +1,50 @@ + +{% block oro_tag_select_row %} + {% import 'OroTagBundle::macros.html.twig' as _tag %} + + {% if resource_granted('oro_tag_assign_unassign') %} +
    +
    +
    + {{ _tag.tagSortActions() }} +
    +
    +
    + {{ form_row(form.autocomplete) }} + {% endif %} + + {# show all tags #} + {#{{ form_row(form.autocomplete) }}#} + + {# show field with mine tags#} + {#{% if resource_granted('oro_tag_assign_unassign') %}#} + {#{{ form_row(form.autocomplete) }}#} + {#{% endif %}#} + + {##} + +{% endblock %} + + {% block oro_combobox_dataconfig_multi_autocomplete %} {{ block('oro_combobox_dataconfig_autocomplete') }} @@ -7,7 +54,7 @@ return this.name.localeCompare(term) === 0; }).length === 0 ) { - {% if not resource_granted('oro_tag_create') %} + {% if resource_granted('oro_tag_create') %} return null; {% else %} return { @@ -17,4 +64,5 @@ {% endif %} } } + {% endblock %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig index 036378b7754..025965473d7 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig @@ -17,37 +17,6 @@ {% endmacro %} -{# - Render input for tags -#} -{% macro tagInputField(label, form, defaultClass) %} - -
    -
    -
    - {% import _self as _ %} - {{ _.tagSortActions() }} -
    -
    -
    - - {{ form_row(form.tags) }} - - - - -{% endmacro %} - {# Render sort actions list #} diff --git a/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig b/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig index 9190a3b3dbd..1a15e0e8b8a 100644 --- a/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig +++ b/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig @@ -1,5 +1,4 @@ {% extends 'OroUIBundle:actions:update.html.twig' %} -{% import 'OroTagBundle::macros.html.twig' as _tag %} {% set format = oro_config_value('oro_user.name_format') %} {% form_theme form with ['OroUIBundle:Form:fields.html.twig', 'OroFormBundle:Form:fields.html.twig', 'OroTagBundle:Form:fields.html.twig'] %} @@ -90,7 +89,7 @@ 'data': [ form_row(form.email), UI.collectionField(form.emails, 'Additional emails', 'Add another email'), - _tag.tagInputField('Tags', form) + form_row(form.tags), ] } ] From 4f3882c63fba0d5b1a5181d3373c98083f7360a3 Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Thu, 1 Aug 2013 13:44:02 +0300 Subject: [PATCH 032/541] fix doctrine convert-mapping --- .../DataAuditBundle/EventListener/EntitySubscriber.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php index f35e36df63d..678a2d03848 100644 --- a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php +++ b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php @@ -60,7 +60,11 @@ public function onFlush(OnFlushEventArgs $event) */ public function loadClassMetadata(LoadClassMetadataEventArgs $event) { - if ($metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata())) { + $doctrineMetadata = $event->getClassMetadata(); + + if ($doctrineMetadata->getReflectionClass() + && $metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata()) + ) { $this->loggableManager->addConfig($metadata); } } From 3294db9c717702478825bd97ceada3d7d0045e34 Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Thu, 1 Aug 2013 13:51:02 +0300 Subject: [PATCH 033/541] fix doctrine convert-mapping --- .../Bundle/DataAuditBundle/EventListener/EntitySubscriber.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php index 678a2d03848..ad5f8aeda0d 100644 --- a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php +++ b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php @@ -60,9 +60,7 @@ public function onFlush(OnFlushEventArgs $event) */ public function loadClassMetadata(LoadClassMetadataEventArgs $event) { - $doctrineMetadata = $event->getClassMetadata(); - - if ($doctrineMetadata->getReflectionClass() + if ($event->getClassMetadata()->getReflectionClass() && $metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata()) ) { $this->loggableManager->addConfig($metadata); From 1b0c678ac7e521544241e3463f42204d4f8f867a Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Thu, 1 Aug 2013 12:42:19 +0000 Subject: [PATCH 034/541] BAP-1232: Wrong behaviour on adding new tags to entity - refactoring tag manager --- .../Bundle/TagBundle/Entity/TagManager.php | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index ec16f363940..dca56f554ef 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -186,9 +186,21 @@ public function saveTagging(Taggable $resource) } } + if (sizeof($tagsToRemove)) { + $this->deleteTaggingByParams( + $tagsToRemove, + get_class($resource), + $resource->getTaggableId(), + $this->getUser()->getId() + ); + } + // process if current user allowed to remove other's tag links if ($this->aclManager->isResourceGranted(self::ACL_RESOURCE_ID_KEY)) { $newAllTags = new ArrayCollection($newTags['all']); + // get 'not mine' taggings + $oldTags = $this->getTagging($resource, $this->getUser()->getId(), true); + $tagsToRemove = array(); foreach ($oldTags as $oldTag) { $callback = function ($index, $newTag) use ($oldTag) { @@ -199,23 +211,14 @@ public function saveTagging(Taggable $resource) $tagsToRemove[] = $oldTag->getId(); } } - } - if (sizeof($tagsToRemove)) { - $builder = $this->em->createQueryBuilder(); - $builder - ->delete($this->taggingClass, 't') - ->where($builder->expr()->in('t.tag', $tagsToRemove)) - ->andWhere('t.entityName = :entityName') - ->setParameter('entityName', get_class($resource)) - ->andWhere('t.recordId = :recordId') - ->setParameter('recordId', $resource->getTaggableId()) - - ->andWhere('t.createdBy = :createdBy') - ->setParameter('createdBy', $this->getUser()->getId()) - - ->getQuery() - ->getResult(); + if (count($tagsToRemove) > 0) { + $this->deleteTaggingByParams( + $tagsToRemove, + get_class($resource), + $resource->getTaggableId() + ); + } } foreach ($tagsToAdd as $tag) { @@ -251,9 +254,10 @@ public function loadTagging(Taggable $resource) * * @param Taggable $resource Taggable resource * @param null|int $createdBy + * @param bool $all * @return array */ - protected function getTagging(Taggable $resource, $createdBy = null) + protected function getTagging(Taggable $resource, $createdBy = null, $all = false) { $qb = $this->em ->createQueryBuilder() @@ -266,7 +270,7 @@ protected function getTagging(Taggable $resource, $createdBy = null) ->setParameter('entityName', get_class($resource)); if (!is_null($createdBy)) { - $qb->where('t2.createdBy = :createdBy') + $qb->where('t2.createdBy ' . ($all ? '!=' : '=') . ' :createdBy') ->setParameter('createdBy', $createdBy); } @@ -287,7 +291,6 @@ public function deleteTagging(Taggable $resource) ->where('t.entityName = :entityName') ->setParameter('entityName', get_class($resource)) - ->andWhere('t.recordId = :id') ->setParameter('id', $resource->getTaggableId()) @@ -302,6 +305,31 @@ public function deleteTagging(Taggable $resource) return $this; } + /** + * @param $tagIds + * @param $entityName + * @param $recordsId + * @param null $createdBy + */ + public function deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy = null) + { + $builder = $this->em->createQueryBuilder(); + $builder + ->delete($this->taggingClass, 't') + ->where($builder->expr()->in('t.tag', $tagIds)) + ->andWhere('t.entityName = :entityName') + ->setParameter('entityName', $entityName) + ->andWhere('t.recordId = :recordId') + ->setParameter('recordId', $recordId); + + if (!is_null($createdBy)) { + $builder->andWhere('t.createdBy = :createdBy') + ->setParameter('createdBy', $createdBy); + } + + $builder->getQuery()->getResult(); + } + /** * Creates a new Tag object * From 30d48fa6a0bc7193587493200c858d16e4dfbb1e Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Thu, 1 Aug 2013 17:03:17 +0300 Subject: [PATCH 035/541] EntityConfigBundle update test --- .../EntityConfigBundle/Entity/ConfigLog.php | 2 +- .../Entity/Repository/ConfigLogRepository.php | 39 -------- .../Metadata/Annotation/Configurable.php | 4 +- .../Tests/Unit/AbstractEntityManagerTest.php | 2 +- .../Tests/Unit/Audit/AuditManagerTest.php | 90 +++++++++++++++++++ .../Tests/Unit/Entity/ConfigLogTest.php | 71 +++++++++++++++ .../Tests/Unit/Entity/ConfigTest.php | 17 ++++ .../Tests/Unit/Event/FlushConfigEventTest.php | 31 +++++++ .../Unit/Event/PersistConfigEventTest.php | 35 ++++++++ .../Metadata/Annotation/ConfigurableTest.php | 67 ++++++++++++-- 10 files changed, 309 insertions(+), 49 deletions(-) delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FlushConfigEventTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php index bd2fc61b449..ae19d48c8f2 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php @@ -9,7 +9,7 @@ /** * @ORM\Table(name="oro_config_log") - * @ORM\Entity(repositoryClass="Oro\Bundle\EntityConfigBundle\Entity\Repository\ConfigLogRepository") + * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ class ConfigLog diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php b/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php deleted file mode 100644 index 54faacd19a0..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php +++ /dev/null @@ -1,39 +0,0 @@ -createQueryBuilder('cl') - ->select('cld.diff as diff, cld.className as className, cld.fieldName as fieldName, cld.scope as scope') - ->addSelect('cl.id as logId') - ->addSelect('u.username as username') - ->leftJoin('cl.diffs', 'cld') - ->leftJoin('cl.user', 'u'); - - if (!$className) { - return $qb; - } - - $qb->setParameter('className', $className); - $qb->andWhere('cld.className = :className'); - - if ($fieldName) { - $qb->setParameter('fieldName', $fieldName); - $qb->andWhere('cld.fieldName = :fieldName'); - } else { - $qb->andWhere('cld.fieldName IS NULL'); - } - - if ($scope) { - $qb->setParameter('scope', $scope); - $qb->andWhere('cld.scope = :scope'); - } - - return $qb; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php index 6d7ff5bc68c..52721ceb09a 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php @@ -33,7 +33,9 @@ public function __construct(array $data) } if (!is_array($this->defaultValues)) { - throw new AnnotationException(sprintf('Annotation "Configurable" parameter "defaultValues" expect "array" give "%s"', $this->defaultValues)); + throw new AnnotationException( + sprintf('Annotation "Configurable" parameter "defaultValues" expect "array" but "%s" given', gettype($this->defaultValues)) + ); } if (!in_array($this->viewMode, array(AbstractConfig::MODE_VIEW_DEFAULT, AbstractConfig::MODE_VIEW_HIDDEN, AbstractConfig::MODE_VIEW_READONLY))) { diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php index 3e2b8bcb46c..2b5adbc55f2 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php @@ -14,7 +14,7 @@ abstract class AbstractEntityManagerTest extends OrmTestCase */ protected $em; - public function setUp() + protected function setUp() { $reader = new AnnotationReader(); diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php new file mode 100644 index 00000000000..bdebef25a55 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php @@ -0,0 +1,90 @@ +getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + + $token = $this->getMockForAbstractClass('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUser')->will($this->returnValue($user)); + + $securityContext = $this->getMockForAbstractClass('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); + + $securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') + ->disableOriginalConstructor() + ->getMock(); + $securityProxy->expects($this->any())->method('getService')->will($this->returnValue($securityContext)); + + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $provider = new ConfigProvider($configManager, new EntityConfigContainer('testScope', array())); + + $configManager->expects($this->any())->method('em')->will($this->returnValue($em)); + + $configManager->expects($this->any())->method('getUpdatedEntityConfig')->will( + $this->returnValue( + array( + new EntityConfig('testClass', 'testScope'), + new FieldConfig('testClass', 'testField', 'string', 'testScope'), + ) + ) + ); + $configManager->expects($this->any())->method('getUpdatedFieldConfig')->will($this->returnValue(array())); + $configManager->expects($this->any())->method('getConfigChangeSet')->will($this->returnValue(array('key' => 'value'))); + $configManager->expects($this->any())->method('getProvider')->will($this->returnValue($provider)); + + $this->auditManager = new AuditManager($configManager, $securityProxy); + } + + protected function tearDown() + { + $this->auditManager = null; + } + + public function testLog() + { + $this->auditManager->log(); + } + + public function testLogWithoutUser() + { + $securityContext = $this->getMockForAbstractClass('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext->expects($this->any())->method('getToken'); + + $securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') + ->disableOriginalConstructor() + ->getMock(); + $securityProxy->expects($this->any())->method('getService')->will($this->returnValue($securityContext)); + + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $auditManager = new AuditManager($configManager, $securityProxy); + + $auditManager->log(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php new file mode 100644 index 00000000000..13684e7085a --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php @@ -0,0 +1,71 @@ +configLog = new ConfigLog(); + $this->configLogDiff = new ConfigLogDiff(); + } + + protected function tearDown() + { + $this->configLog = null; + $this->configLogDiff = null; + } + + public function testConfigLog() + { + $this->assertEmpty($this->configLog->getId()); + + $data = new \DateTime(); + $this->configLog->setLoggedAt($data); + $this->assertEquals($data, $this->configLog->getLoggedAt()); + + $this->configLog->setLoggedAt(null); + $this->configLog->prePersist(); + $this->assertInstanceOf('\DateTime', $this->configLog->getLoggedAt()); + + $this->configLog->setUser('user'); + $this->assertEquals('user', $this->configLog->getUser()); + + $this->configLog->addDiff($this->configLogDiff); + $this->assertEquals($this->configLogDiff, $this->configLog->getDiffs()->first()); + + $diffsCollection = new ArrayCollection(array($this->configLogDiff)); + $this->configLog->setDiffs($diffsCollection); + $this->assertEquals($diffsCollection, $this->configLog->getDiffs()); + } + + public function testConfigDiff() + { + $this->assertEmpty($this->configLogDiff->getId()); + + $this->configLogDiff->setLog($this->configLog); + $this->assertEquals($this->configLog, $this->configLogDiff->getLog()); + + $this->configLogDiff->setClassName('className'); + $this->assertEquals('className', $this->configLogDiff->getClassName()); + + $this->configLogDiff->setFieldName('fieldName'); + $this->assertEquals('fieldName', $this->configLogDiff->getFieldName()); + + $this->configLogDiff->setScope('scope'); + $this->assertEquals('scope', $this->configLogDiff->getScope()); + + $this->configLogDiff->setDiff(array('key' => 'value')); + $this->assertEquals(array('key' => 'value'), $this->configLogDiff->getDiff()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php index b9f9c37c18e..fbe84ae3a52 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -51,6 +51,9 @@ public function testProperties() /** test ConfigField */ $this->assertEmpty($this->configField->getId()); + $this->configField->setMode(ConfigField::MODE_VIEW_READONLY); + $this->assertEquals(ConfigField::MODE_VIEW_READONLY, $this->configField->getMode()); + /** test ConfigValue */ $this->assertEmpty($this->configValue->getId()); $this->assertEmpty($this->configValue->getScope()); @@ -60,6 +63,9 @@ public function testProperties() $this->assertFalse($this->configValue->getSerializable()); + $this->configValue->setSerializable(true); + $this->assertTrue($this->configValue->getSerializable()); + $this->assertEmpty($this->configValue->getEntity()); $this->configValue->setEntity($this->configEntity); @@ -161,5 +167,16 @@ public function testToFromArray() ), $this->configField->toArray('datagrid') ); + + $this->configEntity->addValue(new ConfigValue('is_searchable', 'datagrid', false)); + $this->configEntity->fromArray('datagrid', $values, $serializable); + $this->assertEquals( + array( + 'is_searchable' => 1, + 'is_sortable' => 0, + 'doctrine' => $this->configValue + ), + $this->configEntity->toArray('datagrid') + ); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FlushConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FlushConfigEventTest.php new file mode 100644 index 00000000000..b225648548b --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FlushConfigEventTest.php @@ -0,0 +1,31 @@ +configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + } + + public function testEvent() + { + $event = new FlushConfigEvent($this->configManager); + $this->assertEquals($this->configManager, $event->getConfigManager()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php new file mode 100644 index 00000000000..f2ed8674ecd --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php @@ -0,0 +1,35 @@ +configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + } + + public function testEvent() + { + $config = new EntityConfig('testClass', 'testScope'); + $event = new PersistConfigEvent($config, $this->configManager); + + $this->assertEquals($config, $event->getConfig()); + $this->assertEquals($this->configManager, $event->getConfigManager()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php index d961b750e84..6d6b757b50c 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php @@ -2,20 +2,73 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Metadata\Annotation; +use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; class ConfigurableTest extends \PHPUnit_Framework_TestCase { - public function testConstruct() + /** + * @dataProvider providerTrue + */ + public function testTrue(array $data) { - $annot = new Configurable(array('value' => 'hidden')); - $this->assertEquals('hidden', $annot->viewMode); + $annot = new Configurable($data); + $this->assertEquals(AbstractConfig::MODE_VIEW_HIDDEN, $annot->viewMode); + $this->assertEquals('symfony_route_name', $annot->routeName); + $this->assertEquals(array('key' => 'value'), $annot->defaultValues); + } - $annot = new Configurable(array('viewMode' => 'hidden')); - $this->assertEquals('hidden', $annot->viewMode); + /** + * @dataProvider providerFalse + */ + public function testFalse(array $data, $exceptionClass, $exceptionMessage) + { + $this->setExpectedException($exceptionClass, $exceptionMessage); - $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\AnnotationException'); + $annot = new Configurable($data); + } - $annot = new Configurable(array('viewMode' => 'wrong_value')); + public function providerTrue() + { + return array( + array( + array( + 'value' => AbstractConfig::MODE_VIEW_HIDDEN, + 'routeName' => 'symfony_route_name', + 'defaultValues' => array('key' => 'value'), + ), + ), + array( + array( + 'viewMode' => AbstractConfig::MODE_VIEW_HIDDEN, + 'routeName' => 'symfony_route_name', + 'defaultValues' => array('key' => 'value'), + ), + ) + ); + } + + public function providerFalse() + { + return array( + array( + array( + 'viewMode' => 'wrong_value', + 'routeName' => 'symfony_route_name', + 'defaultValues' => array('key' => 'value'), + ), + 'Oro\Bundle\EntityConfigBundle\Exception\AnnotationException', + 'Annotation "Configurable" give invalid parameter "viewMode" : "wrong_value"' + ), + array( + array( + 'viewMode' => AbstractConfig::MODE_VIEW_HIDDEN, + 'routeName' => 'symfony_route_name', + 'defaultValues' => 'wrong_value', + ), + 'Oro\Bundle\EntityConfigBundle\Exception\AnnotationException', + 'Annotation "Configurable" parameter "defaultValues" expect "array" but "string" given' + ) + ); } } From dda963ff40fb9b724b75071a6e7c543223ba970b Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Thu, 1 Aug 2013 17:25:28 +0200 Subject: [PATCH 036/541] PIM-873 : upgrade BAP to alpha4 --- composer.json | 3 +- .../Bundle/AddressBundle/Entity/Country.php | 4 +- .../Bundle/AddressBundle/Entity/Region.php | 4 +- .../Functional/API/RestAddressTypeApiTest.php | 66 +++ .../Tests/Functional/API/RestApiTest.php | 14 +- .../Functional/API/SoapAddressTypeApiTest.php | 64 ++ .../Tests/Functional/API/SoapApiTest.php | 14 +- .../AsseticBundle/Factory/OroAssetManager.php | 37 +- .../Unit/Factory/OroAssetManagerTest.php | 14 +- .../EventListener/EntitySubscriber.php | 6 +- .../Loggable/LoggableManager.php | 57 +- .../Resources/config/entity_config.yml | 44 ++ .../Resources/config/navigation.yml | 4 +- .../Resources/config/services.yml | 12 +- .../views/change_history_link.html.twig | 2 +- .../Tests/Functional/SoapDataAuditApiTest.php | 2 +- .../Unit/Loggable/LoggableManagerTest.php | 11 +- .../Api/Rest/EmailTemplateController.php | 121 ++++ .../Controller/EmailTemplateController.php | 116 ++++ .../DataFixtures/ORM/AbstractEmailFixture.php | 80 +++ .../DataFixtures/ORM/LoadEmailTemplates.php | 16 + .../data/emails/user/update_user.html.twig | 5 + .../Datagrid/EmailTemplateDatagridManager.php | 201 +++++++ .../DependencyInjection/Configuration.php | 26 + .../DependencyInjection/OroEmailExtension.php | 29 + .../EmailBundle/Entity/EmailTemplate.php | 370 ++++++++++++ .../Entity/EmailTemplateTranslation.php | 21 + .../Repository/EmailTemplateRepository.php | 34 ++ .../BuildTemplateFormSubscriber.php | 139 +++++ .../Form/Handler/EmailTemplateHandler.php | 85 +++ .../Form/Type/EmailTemplateApiType.php | 42 ++ .../Form/Type/EmailTemplateSelectType.php | 67 +++ .../Type/EmailTemplateTranslationType.php | 37 ++ .../Form/Type/EmailTemplateType.php | 106 ++++ src/Oro/Bundle/EmailBundle/OroEmailBundle.php | 9 + .../EmailBundle/Resources/config/assets.yml | 8 + .../EmailBundle/Resources/config/datagrid.yml | 13 + .../Resources/config/navigation.yml | 19 + .../EmailBundle/Resources/config/routing.yml | 14 + .../EmailBundle/Resources/config/services.yml | 83 +++ .../Resources/config/validation.yml | 14 + .../Resources/public/css/style.css | 12 + .../js/collections/templates.updater.js | 24 + .../public/js/models/templates.updater.js | 10 + .../public/js/views/templates.updater.js | 40 ++ .../Resources/translations/datagrid.en.yml | 21 + .../Resources/translations/messages.en.yml | 8 + .../Resources/translations/validators.en.yml | 5 + .../views/EmailTemplate/index.html.twig | 15 + .../views/EmailTemplate/update.html.twig | 103 ++++ .../Resources/views/Form/fields.html.twig | 20 + .../Tests/Unit/Entity/EmailTemplateTest.php | 65 ++ .../EmailTemplateRepositoryTest.php | 92 +++ .../BuildTemplateFormSubscriberTest.php | 246 ++++++++ .../Form/Handler/EmailTemplateHandlerTest.php | 139 +++++ .../Form/Type/EmailTemplateApiTestTest.php | 51 ++ .../Form/Type/EmailTemplateSelectTypeTest.php | 71 +++ .../Unit/Form/Type/EmailTemplateTest.php | 50 ++ .../Type/EmailTemplateTranslationTypeTest.php | 43 ++ .../Bundle/EmailBundle/Tests/bootstrap.php | 14 + src/Oro/Bundle/EmailBundle/composer.json | 22 + src/Oro/Bundle/EmailBundle/readme.md | 0 .../EntityBundle/Datagrid/EntityDatagrid.php | 26 + .../DependencyInjection/Configuration.php | 28 + .../OroEntityExtension.php | 62 ++ .../EntityBundle/Entity/AuditCommit.php | 160 +++++ .../Bundle/EntityBundle/Entity/AuditDiff.php | 152 +++++ .../Exception/AnnotationException.php | 7 + .../Exception/RuntimeException.php | 7 + .../EntityBundle/Extend/ExtendManager.php | 31 + .../Bundle/EntityBundle/OroEntityBundle.php | 9 + src/Oro/Bundle/EntityBundle/README.md | 2 + .../Resources/config/entity_config.yml | 124 ++++ .../Resources/config/form_type.yml | 8 + .../EntityBundle/Resources/config/routing.yml | 5 + .../Resources/config/services.yml | 10 + src/Oro/Bundle/EntityBundle/composer.json | 28 + .../EntityConfigBundle/Audit/AuditManager.php | 102 ++++ .../Cache/CacheInterface.php | 26 + .../EntityConfigBundle/Cache/FileCache.php | 58 ++ .../Command/BaseCommand.php | 18 + .../Command/UpdateCommand.php | 38 ++ .../Config/AbstractConfig.php | 93 +++ .../Config/ConfigInterface.php | 54 ++ .../Config/EntityConfig.php | 149 +++++ .../Config/EntityConfigInterface.php | 12 + .../EntityConfigBundle/Config/FieldConfig.php | 147 +++++ .../Config/FieldConfigInterface.php | 16 + .../EntityConfigBundle/ConfigManager.php | 559 ++++++++++++++++++ .../Controller/AuditController.php | 110 ++++ .../Controller/ConfigController.php | 288 +++++++++ .../Datagrid/AuditDatagrid.php | 150 +++++ .../Datagrid/AuditDatagridManager.php | 29 + .../Datagrid/AuditFieldDatagridManager.php | 40 ++ .../Datagrid/ConfigDatagridManager.php | 328 ++++++++++ .../Datagrid/EntityFieldsDatagridManager.php | 267 +++++++++ .../Compiler/EntityConfigPass.php | 50 ++ .../Compiler/ServiceProxyPass.php | 47 ++ .../DependencyInjection/Configuration.php | 23 + .../EntityConfigContainer.php | 296 ++++++++++ .../OroEntityConfigExtension.php | 92 +++ .../Proxy/ServiceProxy.php | 34 ++ .../Entity/AbstractConfig.php | 218 +++++++ .../Entity/ConfigEntity.php | 122 ++++ .../EntityConfigBundle/Entity/ConfigField.php | 127 ++++ .../EntityConfigBundle/Entity/ConfigLog.php | 137 +++++ .../Entity/ConfigLogDiff.php | 158 +++++ .../EntityConfigBundle/Entity/ConfigValue.php | 210 +++++++ .../Entity/Repository/ConfigLogRepository.php | 39 ++ .../EntityConfigBundle/Event/Events.php | 16 + .../Event/FlushConfigEvent.php | 31 + .../Event/NewEntityEvent.php | 47 ++ .../Event/NewFieldEvent.php | 77 +++ .../Event/PersistConfigEvent.php | 44 ++ .../Exception/AnnotationException.php | 7 + .../Exception/RuntimeException.php | 7 + .../Form/EventListener/ConfigSubscriber.php | 61 ++ .../Form/Extension/ConfigFormExtension.php | 51 ++ .../Form/Type/ConfigEntityType.php | 74 +++ .../Form/Type/ConfigFieldType.php | 112 ++++ .../Form/Type/ConfigType.php | 54 ++ .../Form/Type/EntityConfigTypeInterface.php | 7 + .../Form/Type/FieldConfigTypeInterface.php | 8 + .../Metadata/Annotation/Configurable.php | 43 ++ .../Metadata/ConfigClassMetadata.php | 74 +++ .../Metadata/Driver/AnnotationDriver.php | 51 ++ .../OroEntityConfigBundle.php | 18 + .../Provider/ConfigProvider.php | 184 ++++++ .../Provider/ConfigProviderInterface.php | 8 + src/Oro/Bundle/EntityConfigBundle/README.md | 2 + .../Resources/config/datagrid.yml | 54 ++ .../Resources/config/form_type.yml | 8 + .../Resources/config/metadata.yml | 22 + .../Resources/config/navigation.yml | 19 + .../Resources/config/routing.yml | 4 + .../Resources/config/services.yml | 46 ++ .../Resources/doc/config_provider.md | 40 ++ .../Resources/doc/configuration.md | 115 ++++ .../EntityConfigBundle/Resources/doc/index.md | 19 + .../Resources/doc/schema/entity.yml | 38 ++ .../Resources/views/Audit/audit.html.twig | 14 + .../Resources/views/Audit/data.html.twig | 22 + .../views/Config/fieldUpdate.html.twig | 51 ++ .../Resources/views/Config/index.html.twig | 20 + .../Resources/views/Config/update.html.twig | 44 ++ .../Resources/views/Config/view.html.twig | 103 ++++ .../Tests/Unit/AbstractEntityManagerTest.php | 40 ++ .../Tests/Unit/Cache/FileCacheTest.php | 62 ++ .../Tests/Unit/Config/EntityConfigTest.php | 43 ++ .../Tests/Unit/Config/FieldConfigTest.php | 67 +++ .../Tests/Unit/ConfigManagerTest.php | 265 +++++++++ .../Compiler/EntityConfigPassTest.php | 100 ++++ .../DependencyInjection/ConfigurationTest.php | 27 + .../EntityConfigContainerTest.php | 77 +++ .../Tests/Unit/Entity/ConfigTest.php | 165 ++++++ .../Unit/Event/EntityConfigEventTest.php | 34 ++ .../Tests/Unit/Event/FieldConfigEventTest.php | 36 ++ .../Tests/Unit/Fixture/DemoEntity.php | 134 +++++ .../Unit/Fixture/NoConfigurableEntity.php | 28 + .../FoundEntityConfigRepository.php | 37 ++ .../NotFoundEntityConfigRepository.php | 13 + .../Tests/Unit/Fixture/entity_config.yml | 107 ++++ .../Unit/Form/Type/ConfigEntityTypeTest.php | 77 +++ .../Unit/Form/Type/ConfigFieldTypeTest.php | 94 +++ .../Metadata/Annotation/ConfigurableTest.php | 21 + .../Tests/Unit/Metadata/ClassMetadataTest.php | 34 ++ .../Tests/Unit/OroEntityConfigBundleTest.php | 37 ++ .../Unit/Provider/ConfigProviderTest.php | 100 ++++ .../Bundle/EntityConfigBundle/composer.json | 29 + .../EntityConfigBundle/phpunit.xml.dist | 33 ++ .../Command/BackupCommand.php | 118 ++++ .../Command/CheckDynamicCommand.php | 50 ++ .../Command/GenerateCommand.php | 55 ++ .../Controller/ApplyController.php | 162 +++++ .../Controller/ConfigEntityGridController.php | 117 ++++ .../Controller/ConfigFieldGridController.php | 141 +++++ .../Databases/DatabaseInterface.php | 29 + .../Databases/MySQLDatabase.php | 71 +++ .../Databases/PostgresDatabase.php | 70 +++ .../DependencyInjection/Configuration.php | 39 ++ .../OroEntityExtendExtension.php | 75 +++ .../Entity/ExtendEntityInterface.php | 14 + .../Entity/ExtendProxyInterface.php | 16 + .../EventListener/ConfigSubscriber.php | 94 +++ .../EventListener/DoctrineSubscriber.php | 98 +++ .../Exception/RuntimeException.php | 7 + .../Extend/ExtendManager.php | 162 +++++ .../Extend/ExtendRepository.php | 60 ++ .../Extend/Factory/ConfigFactory.php | 62 ++ .../Extend/ProxyObjectFactory.php | 103 ++++ .../Form/Extension/ExtendEntityExtension.php | 62 ++ .../Form/Type/FieldType.php | 58 ++ .../Form/Type/UniqueKeyCollectionType.php | 50 ++ .../Form/Type/UniqueKeyType.php | 56 ++ .../Mapping/ExtendClassMetadataFactory.php | 17 + .../Metadata/Annotation/Extend.php | 13 + .../Metadata/Driver/AnnotationDriver.php | 44 ++ .../Metadata/ExtendClassMetadata.php | 50 ++ .../OroEntityExtendBundle.php | 45 ++ src/Oro/Bundle/EntityExtendBundle/README.md | 2 + .../Resources/config/assets.yml | 7 + .../Resources/config/entity_config.yml | 165 ++++++ .../Resources/config/metadata.yml | 22 + .../Resources/config/routing.yml | 4 + .../Resources/config/services.yml | 50 ++ .../Resources/public/css/extend.css | 3 + .../Resources/public/js/extend.apply.js | 39 ++ .../public/js/extend.field.create.js | 17 + .../Resources/translations/messages.en.yml | 3 + .../Resources/views/Apply/apply.html.twig | 36 ++ .../views/ConfigEntityGrid/unique.html.twig | 87 +++ .../views/ConfigFieldGrid/create.html.twig | 54 ++ .../Tools/Generator/Generator.php | 326 ++++++++++ .../EntityExtendBundle/Tools/Schema.php | 114 ++++ .../Bundle/EntityExtendBundle/composer.json | 30 + .../Resources/public/css/oro.filter.css | 2 +- .../public/js/app/filter/choicefilter.js | 2 +- .../public/js/app/filter/datefilter.js | 2 +- .../public/js/app/filter/multiselectfilter.js | 2 +- .../public/js/app/filter/selectfilter.js | 2 +- .../public/js/app/filter/textfilter.js | 4 +- .../Resources/translations/jsmessages.en.yml | 2 + .../AttributeType/AbstractAttributeType.php | 12 +- .../AttributeType/AbstractOptionType.php | 2 +- .../AttributeType/AttributeTypeFactory.php | 1 + .../AttributeType/AttributeTypeInterface.php | 2 +- .../AttributeType/BooleanType.php | 3 +- .../AttributeType/DateTimeType.php | 2 +- .../AttributeType/DateType.php | 2 +- .../AttributeType/EmailCollectionType.php | 3 + .../AttributeType/EmailType.php | 2 +- .../AttributeType/FileType.php | 3 +- .../AttributeType/FileUrlType.php | 3 +- .../AttributeType/ImageType.php | 3 +- .../AttributeType/ImageUrlType.php | 3 +- .../AttributeType/IntegerType.php | 2 +- .../AttributeType/MetricType.php | 9 +- .../AttributeType/MoneyType.php | 2 +- .../AttributeType/NumberType.php | 2 +- .../AttributeType/OptionMultiCheckboxType.php | 1 - .../AttributeType/OptionMultiSelectType.php | 2 +- .../AttributeType/OptionSimpleRadioType.php | 2 +- .../AttributeType/OptionSimpleSelectType.php | 2 +- .../AttributeType/PhoneCollectionType.php | 4 + .../AttributeType/PriceType.php | 2 +- .../AttributeType/TextAreaType.php | 2 +- .../AttributeType/TextType.php | 2 +- .../AttributeType/UrlType.php | 2 +- .../Compiler/AddAttributeTypeCompilerPass.php | 3 +- .../Compiler/AddManagerCompilerPass.php | 3 +- .../DependencyInjection/Configuration.php | 2 +- .../OroFlexibleEntityExtension.php | 1 - .../Doctrine/ORM/FlexibleQueryBuilder.php | 56 +- .../FlexibleEntityBundle/Entity/Attribute.php | 2 +- .../Entity/AttributeOption.php | 2 +- .../Entity/AttributeOptionValue.php | 2 +- .../Entity/AttributeTranslation.php | 2 +- .../Entity/Collection.php | 13 +- .../Mapping/AbstractEntityAttribute.php | 3 +- .../Mapping/AbstractEntityAttributeOption.php | 3 +- .../AbstractEntityAttributeOptionValue.php | 3 +- .../Entity/Mapping/AbstractEntityFlexible.php | 15 +- .../Mapping/AbstractEntityFlexibleValue.php | 5 +- .../FlexibleEntityBundle/Entity/Media.php | 2 +- .../FlexibleEntityBundle/Entity/Metric.php | 2 +- .../FlexibleEntityBundle/Entity/Price.php | 4 +- .../Repository/AttributeOptionRepository.php | 2 - .../Entity/Repository/AttributeRepository.php | 2 +- .../Repository/FlexibleEntityRepository.php | 1 + .../Event/AbstractFilterEvent.php | 2 +- .../Event/FilterAttributeEvent.php | 3 +- .../Event/FilterFlexibleEvent.php | 3 +- .../Event/FilterFlexibleValueEvent.php | 3 +- .../EventListener/AddAttributesListener.php | 2 +- .../EventListener/DefaultValueListener.php | 3 +- .../InitializeValuesListener.php | 3 +- .../EventListener/RequiredValueListener.php | 3 +- .../EventListener/ScopableListener.php | 2 +- .../EventListener/TimestampableListener.php | 2 +- .../EventListener/TranslatableListener.php | 2 +- .../FlexibleConfigurationException.php | 3 +- .../Exception/FlexibleQueryException.php | 3 +- .../Exception/HasRequiredValueException.php | 3 +- .../Exception/UnknownAttributeException.php | 3 +- .../EventListener/AttributeTypeSubscriber.php | 3 +- .../CollectionTypeSubscriber.php | 3 + .../EventListener/FlexibleValueSubscriber.php | 7 +- .../Form/Type/AttributeOptionType.php | 3 +- .../Form/Type/AttributeOptionValueType.php | 3 +- .../Form/Type/AttributeType.php | 3 +- .../Form/Type/CollectionAbstract.php | 3 + .../Form/Type/CollectionItemAbstract.php | 3 + .../Form/Type/EmailCollectionType.php | 4 + .../Form/Type/EmailType.php | 4 + .../Form/Type/FlexibleType.php | 4 +- .../Form/Type/FlexibleValueType.php | 3 +- .../Form/Type/MediaType.php | 3 +- .../Form/Type/MetricType.php | 3 +- .../Form/Type/PhoneCollectionType.php | 4 + .../Form/Type/PhoneType.php | 4 + .../Form/Type/PriceType.php | 1 + .../Validator/AttributeConstraintGuesser.php | 9 + .../Validator/ConstraintGuesserInterface.php | 3 + .../Manager/FlexibleManager.php | 3 +- .../Manager/FlexibleManagerRegistry.php | 4 +- .../Manager/SimpleManager.php | 3 +- .../Model/AbstractAttribute.php | 2 +- .../Model/AbstractAttributeOption.php | 2 +- .../Model/AbstractAttributeOptionValue.php | 2 +- .../Model/AbstractFlexible.php | 3 +- .../Model/AbstractFlexibleValue.php | 3 +- .../Model/Behavior/ScopableInterface.php | 4 +- .../Model/Behavior/TimestampableInterface.php | 2 +- .../Model/Behavior/TranslatableInterface.php | 2 +- .../Model/FlexibleInterface.php | 3 +- .../Model/FlexibleValueInterface.php | 4 +- .../AbstractFlexibleEntityManagerTest.php | 2 +- .../Tests/Unit/AbstractOrmTest.php | 2 +- .../Tests/Unit/Entity/CollectionTest.php | 16 +- .../Tests/Unit/Entity/Demo/Flexible.php | 2 +- .../Tests/Unit/Entity/Demo/FlexibleValue.php | 2 +- .../Tests/Unit/Entity/Demo/Simple.php | 1 + .../Tests/Unit/Entity/FlexibleValueTest.php | 3 +- .../Form/Type/AttributeOptionTypeTest.php | 3 +- .../Unit/Form/Type/AttributeTypeTest.php | 3 +- .../Form/Type/EmailCollectionTypeTest.php | 4 + .../Tests/Unit/Form/Type/EmailTypeTest.php | 4 + .../Tests/Unit/Form/Type/FlexibleTypeTest.php | 3 +- .../Unit/Form/Type/FlexibleValueTypeTest.php | 3 + .../Tests/Unit/Form/Type/MediaTypeTest.php | 3 +- .../Tests/Unit/Form/Type/MetricTypeTest.php | 3 +- .../Form/Type/PhoneCollectionTypeTest.php | 4 + .../Tests/Unit/Form/Type/PhoneTypeTest.php | 4 + .../Tests/Unit/Form/Type/PriceTypeTest.php | 3 +- .../AttributeConstraintGuesserTest.php | 3 + .../Manager/FlexibleManagerRegistryTest.php | 2 +- .../Unit/Manager/FlexibleManagerTest.php | 2 +- .../Tests/Unit/Manager/SimpleManagerTest.php | 2 +- .../Twig/FilterAttributesExtensionTest.php | 3 + .../Twig/FilterAttributesExtension.php | 11 +- .../Bundle/FormBundle/Config/BlockConfig.php | 215 +++++++ .../Bundle/FormBundle/Config/FormConfig.php | 93 +++ .../FormBundle/Config/FormConfigInterface.php | 8 + .../FormBundle/Config/SubBlockConfig.php | 130 ++++ .../DependencyInjection/OroFormExtension.php | 1 + .../FormBundle/Exception/RuntimeException.php | 7 + .../EntitiesToIdsTransformer.php | 4 +- .../Form/Extension/DataBlockExtension.php | 51 ++ .../FormBundle/Form/Twig/DataBlocks.php | 178 ++++++ .../Form/Type/OroJquerySelect2HiddenType.php | 12 +- .../FormBundle/Resources/config/assets.yml | 2 +- .../FormBundle/Resources/config/form_type.yml | 16 +- .../FormBundle/Resources/config/services.yml | 7 + .../Bundle/FormBundle/Resources/doc/index.md | 1 + .../doc/reference/ui_datablock_config.md | 99 ++++ .../Resources/public/js/oro.select2.config.js | 8 +- .../Unit/Autocomplete/SearchHandlerTest.php | 20 +- .../Tests/Unit/Config/BlockConfigTest.php | 167 ++++++ .../Tests/Unit/Config/FormConfigTest.php | 101 ++++ .../EntitiesToIdsTransformerTest.php | 5 +- .../EntityToIdTransformerTest.php | 4 +- .../Form/Extension/DataBlockExtensionTest.php | 58 ++ .../Tests/Unit/Form/Twig/DataBlocksTest.php | 138 +++++ .../Form/Type/EntityIdentifierTypeTest.php | 10 +- .../Type/OroJquerySelect2HiddenTypeTest.php | 20 +- .../Bundle/FormBundle/Twig/FormExtension.php | 27 + .../ORM/Flexible/FlexibleOptionsFilter.php | 3 - .../Property/TwigTemplateProperty.php | 45 +- .../js/app/datagrid/action/deleteaction.js | 12 +- .../js/app/datagrid/cell/booleancell.js | 27 +- .../datagrid/listener/columnformlistener.js | 12 +- .../public/js/app/datagrid/pagesize.js | 2 +- .../Resources/public/js/app/datagrid/row.js | 6 +- .../public/js/app/datagrid/toolbar.js | 12 +- .../Resources/translations/jsmessages.en.yml | 20 + .../Resources/translations/jsmessages.es.yml | 20 + .../views/Include/javascript.html.twig | 20 +- .../Flexible/FlexibleOptionsFilterTest.php | 2 +- .../Property/TwigTemplatePropertyTest.php | 44 +- src/Oro/Bundle/ImapBundle/.gitignore | 2 + .../InvalidConfigurationException.php | 7 + .../ImapBundle/Connector/ImapConfig.php | 147 +++++ .../ImapBundle/Connector/ImapConnector.php | 165 ++++++ .../Connector/ImapConnectorFactory.php | 65 ++ .../ImapBundle/Connector/ImapServices.php | 44 ++ .../Connector/ImapServicesFactory.php | 117 ++++ .../Search/AbstractSearchQueryBuilder.php | 109 ++++ .../Search/AbstractSearchStringManager.php | 150 +++++ .../Search/GmailSearchStringManager.php | 129 ++++ .../Connector/Search/SearchQuery.php | 187 ++++++ .../Connector/Search/SearchQueryBuilder.php | 168 ++++++ .../Connector/Search/SearchQueryExpr.php | 181 ++++++ .../Search/SearchQueryExprInterface.php | 7 + .../Connector/Search/SearchQueryExprItem.php | 40 ++ .../SearchQueryExprNamedItemInterface.php | 16 + .../Search/SearchQueryExprOperator.php | 37 ++ .../Connector/Search/SearchQueryExprValue.php | 7 + .../Search/SearchQueryExprValueBase.php | 64 ++ .../Search/SearchQueryExprValueInterface.php | 7 + .../Connector/Search/SearchQueryMatch.php | 24 + .../Search/SearchQueryValueBuilder.php | 13 + .../Connector/Search/SearchStringManager.php | 196 ++++++ .../Search/SearchStringManagerInterface.php | 29 + .../DependencyInjection/Configuration.php | 20 + .../DependencyInjection/OroImapExtension.php | 25 + src/Oro/Bundle/ImapBundle/LICENSE | 19 + .../ImapBundle/Mail/Storage/Attachment.php | 104 ++++ .../Bundle/ImapBundle/Mail/Storage/Body.php | 132 +++++ .../ImapBundle/Mail/Storage/Content.php | 59 ++ .../Exception/InvalidBodyFormatException.php | 7 + .../ImapBundle/Mail/Storage/GmailImap.php | 54 ++ .../Bundle/ImapBundle/Mail/Storage/Imap.php | 159 +++++ .../ImapBundle/Mail/Storage/Message.php | 104 ++++ .../Bundle/ImapBundle/Mail/Storage/Value.php | 46 ++ src/Oro/Bundle/ImapBundle/OroImapBundle.php | 9 + src/Oro/Bundle/ImapBundle/README.md | 69 +++ .../ImapBundle/Resources/config/services.yml | 22 + .../Tests/Unit/Connector/ImapConfigTest.php | 47 ++ .../Unit/Connector/ImapConnectorTest.php | 145 +++++ .../Connector/ImapServicesFactoryTest.php | 62 ++ .../Search/GmailSearchStringManagerTest.php | 167 ++++++ .../Search/SearchQueryBuilderTest.php | 229 +++++++ .../Search/SearchQueryExprItemTest.php | 36 ++ .../Search/SearchQueryExprOperatorTest.php | 27 + .../Search/SearchQueryExprValueTest.php | 31 + .../Unit/Connector/Search/SearchQueryTest.php | 139 +++++ .../Search/SearchStringManagerTest.php | 229 +++++++ .../Unit/Connector/TestFixtures/Imap1.php | 19 + .../Unit/Connector/TestFixtures/Imap2.php | 19 + .../TestFixtures/SearchStringManager1.php | 16 + .../TestFixtures/SearchStringManager2.php | 16 + .../Unit/Mail/Storage/AttachmentTest.php | 238 ++++++++ .../Tests/Unit/Mail/Storage/BodyTest.php | 346 +++++++++++ .../Tests/Unit/Mail/Storage/ContentTest.php | 20 + .../Tests/Unit/Mail/Storage/ValueTest.php | 18 + src/Oro/Bundle/ImapBundle/Tests/bootstrap.php | 14 + src/Oro/Bundle/ImapBundle/composer.json | 23 + .../Controller/Api/ShortcutsController.php | 33 +- .../Controller/ShortcutController.php | 29 +- .../DependencyInjection/Configuration.php | 12 + .../Event/RequestTitleListener.php | 6 +- .../Event/ResponseHashnavListener.php | 5 +- .../Event/ResponseHistoryListener.php | 3 +- .../Menu/AclAwareMenuFactoryExtension.php | 2 - .../Menu/BreadcrumbManager.php | 218 +++++++ .../Provider/TitleService.php | 62 +- .../Resources/config/navigation.yml | 10 +- .../Resources/config/placeholders.yml | 13 +- .../Resources/config/services.yml | 21 +- .../Resources/public/js/hash.navigation.js | 83 ++- .../Resources/public/js/views/favorites.js | 15 +- .../Resources/public/js/views/pagestate.js | 21 +- .../Resources/public/js/views/pinbar.item.js | 12 +- .../Resources/public/js/views/pinbar.js | 74 ++- .../Resources/translations/jsmessages.en.yml | 5 + .../ApplicationMenu/breabcrumbs.html.twig | 1 + .../views/ApplicationMenu/pinButton.html.twig | 10 +- .../views/HashNav/hashNavAjax.html.twig | 1 + .../Resources/views/HashNav/script.js.twig | 5 +- .../Resources/views/Js/pinbar.js.twig | 4 +- .../views/Menu/application_menu.html.twig | 33 +- .../views/Menu/breadcrumbs.html.twig | 7 + .../Resources/views/Menu/pinbar.html.twig | 10 +- .../Resources/views/Menu/shortcuts.html.twig | 10 +- .../Event/ResponseHashnavListenerTest.php | 2 +- .../Tests/Unit/Menu/BreadcrumbManagerTest.php | 155 +++++ .../Tests/Unit/Provider/TitleServiceTest.php | 35 +- .../Tests/Unit/Twig/HashNavExtensionTest.php | 10 + .../Tests/Unit/Twig/MenuExtensionTest.php | 57 +- .../Twig/HashNavExtension.php | 20 +- .../NavigationBundle/Twig/MenuExtension.php | 58 +- .../Command/SpoolSendCommand.php | 38 ++ .../Api/Rest/EmailNotificationController.php | 70 +++ .../EmailNotificationController.php | 99 ++++ .../ORM/LoadDefaultNotificationEvents.php | 38 ++ .../EmailNotificationDatagridManager.php | 292 +++++++++ .../Compiler/EventsCompilerPass.php | 56 ++ .../Compiler/NotificationHandlerPass.php | 30 + .../DependencyInjection/Configuration.php | 26 + .../OroNotificationExtension.php | 29 + .../Entity/EmailNotification.php | 204 +++++++ .../NotificationBundle/Entity/Event.php | 101 ++++ .../Entity/RecipientList.php | 260 ++++++++ .../EmailNotificationRepository.php | 26 + .../Entity/Repository/EventRepository.php | 19 + .../Repository/RecipientListRepository.php | 63 ++ .../Entity/Repository/SpoolItemRepository.php | 15 + .../NotificationBundle/Entity/SpoolItem.php | 95 +++ .../Handler/EmailNotificationHandler.php | 208 +++++++ .../Event/Handler/EventHandlerAbstract.php | 38 ++ .../Event/Handler/EventHandlerInterface.php | 27 + .../Event/NotificationEvent.php | 38 ++ .../Form/Handler/EmailNotificationHandler.php | 64 ++ .../Form/Type/EmailNotificationApiType.php | 42 ++ .../Form/Type/EmailNotificationType.php | 137 +++++ .../Form/Type/RecipientListType.php | 92 +++ .../OroNotificationBundle.php | 42 ++ .../Provider/DoctrineListener.php | 101 ++++ .../Provider/Mailer/DbSpool.php | 123 ++++ .../Provider/NotificationManager.php | 105 ++++ .../Resources/config/datagrid.yml | 13 + .../Resources/config/navigation.yml | 18 + .../Resources/config/routing.yml | 13 + .../Resources/config/services.yml | 175 ++++++ .../Resources/config/validation.yml | 16 + .../Resources/translations/datagrid.en.yml | 15 + .../Resources/translations/messages.en.yml | 9 + .../Resources/translations/validators.en.yml | 5 + .../Datagrid/Property/entityName.html.twig | 2 + .../Datagrid/Property/recipientList.html.twig | 6 + .../views/EmailNotification/index.html.twig | 15 + .../views/EmailNotification/update.html.twig | 88 +++ .../Resources/views/Form/fields.html.twig | 27 + .../Unit/Command/SpoolSendCommandTest.php | 62 ++ .../Compiler/EventsCompilerPassTest.php | 125 ++++ .../Compiler/NotificationHandlerPassTest.php | 72 +++ .../DependencyInjection/ConfigurationTest.php | 15 + .../OroNotificationExtensionTest.php | 19 + .../Unit/Entity/EmailNotificationTest.php | 112 ++++ .../Tests/Unit/Entity/EventTest.php | 45 ++ .../Tests/Unit/Entity/RecipientListTest.php | 178 ++++++ .../EmailNotificationRepositoryTest.php | 85 +++ .../Entity/Repository/EventRepositoryTest.php | 80 +++ .../RecipientListRepositoryTest.php | 115 ++++ .../Tests/Unit/Entity/SpoolItemTest.php | 42 ++ .../Handler/EmailNotificationHandlerTest.php | 244 ++++++++ .../Unit/Event/NotificationEventTest.php | 31 + .../Tests/Unit/Fixtures/Entity/FakeEntity.php | 22 + .../Resources/emails/test.template.html.twig | 0 .../Handler/EmailNotificationHandlerTest.php | 118 ++++ .../Type/EmailNotificationApiTypeTest.php | 54 ++ .../Form/Type/EmailNotificationTypeTest.php | 57 ++ .../Unit/Form/Type/RecipientListTypeTest.php | 60 ++ .../Tests/Unit/OroNotificationBundleTest.php | 30 + .../Tests/Unit/Provider/DbSpoolTest.php | 169 ++++++ .../Unit/Provider/DoctrineListenerTest.php | 77 +++ .../Unit/Provider/NotificationManagerTest.php | 143 +++++ .../NotificationBundle/Tests/bootstrap.php | 14 + .../Bundle/NotificationBundle/composer.json | 21 + src/Oro/Bundle/NotificationBundle/readme.md | 0 .../Api/Rest/BusinessUnitController.php | 140 +++++ .../Api/Soap/BusinessUnitController.php | 94 +++ .../Controller/BusinessUnitController.php | 180 ++++++ .../DataFixtures/ORM/LoadOrganizationData.php | 23 + .../Datagrid/BusinessUnitDatagridManager.php | 231 ++++++++ .../BusinessUnitUpdateUserDatagridManager.php | 135 +++++ .../BusinessUnitViewUserDatagridManager.php | 68 +++ .../DependencyInjection/Configuration.php | 20 + .../OroOrganizationExtension.php | 25 + .../Entity/BusinessUnit.php | 389 ++++++++++++ .../Entity/Manager/BusinessUnitManager.php | 55 ++ .../Entity/Organization.php | 131 ++++ .../Repository/BusinessUnitRepository.php | 74 +++ .../Form/Handler/BusinessUnitHandler.php | 111 ++++ .../Form/Type/BusinessUnitApiType.php | 44 ++ .../Form/Type/BusinessUnitType.php | 110 ++++ .../OroOrganizationBundle.php | 9 + src/Oro/Bundle/OrganizationBundle/README.md | 10 + .../Resources/config/datagrid.yml | 34 ++ .../Resources/config/navigation.yml | 19 + .../Resources/config/routing.yml | 11 + .../Resources/config/services.yml | 53 ++ .../Resources/config/validation.yml | 14 + .../BusinessUnit/businessUnitsTree.html.twig | 13 + .../views/BusinessUnit/index.html.twig | 11 + .../views/BusinessUnit/update.html.twig | 104 ++++ .../views/BusinessUnit/view.html.twig | 85 +++ .../DependencyInjection/ConfigurationTest.php | 19 + .../Tests/Unit/Entity/BusinessUnitTest.php | 121 ++++ .../Tests/Unit/Entity/OrganizationTest.php | 47 ++ .../Form/Handler/BusinessUnitHandlerTest.php | 128 ++++ .../Unit/Form/Type/BusinessUnitTypeTest.php | 45 ++ .../Bundle/OrganizationBundle/composer.json | 23 + .../Resources/config/navigation.yml | 2 +- .../Resources/public/js/searchBar.js | 52 +- .../views/Search/searchBar.html.twig | 56 +- .../Entity/Repository/SegmentRepository.php | 2 +- .../Bundle/SoapBundle/Entity/RequestFix.php | 18 +- .../TagBundle/Resources/config/navigation.yml | 15 +- .../Resources/views/Search/result.html.twig | 4 +- .../Tests/Functional/ControllersTest.php | 58 ++ .../Pages/AbstractEntity.php | 27 + .../Pages/Objects/Account.php | 71 ++- .../Pages/Objects/Accounts.php | 1 + .../Pages/Objects/Contact.php | 258 ++++++-- .../Pages/Objects/Contacts.php | 1 + .../Pages/Objects/Group.php | 19 +- .../Pages/Objects/Navigation.php | 6 +- .../Pages/Objects/Role.php | 17 +- .../Pages/Objects/Roles.php | 1 + .../Pages/Objects/Search.php | 48 +- .../TestFrameworkBundle/Pages/Objects/Tag.php | 43 ++ .../Pages/Objects/Tags.php | 62 ++ .../Pages/Objects/User.php | 49 +- .../Pages/Objects/Users.php | 1 + .../Bundle/TestFrameworkBundle/Pages/Page.php | 17 +- .../TestFrameworkBundle/Test/TestListener.php | 2 +- .../TestFrameworkBundle/Test/ToolsAPI.php | 9 +- .../Tests/Selenium/Groups/GroupsTest.php | 8 +- .../Tests/Selenium/Login/LoginFormTest.php | 6 +- .../Selenium/Navigation/NavigationTest.php | 2 +- .../Tests/Selenium/Roles/RolesTest.php | 2 +- .../Tests/Selenium/Tags/TagsTest.php | 86 +++ .../Tests/Selenium/User/UsersTest.php | 10 +- .../Command/OroTranslationDumpCommand.php | 71 +++ .../Controller/Controller.php | 103 ++++ .../DependencyInjection/Configuration.php | 35 ++ .../OroTranslationExtension.php | 10 + .../Form/Type/TranslatableEntityType.php | 3 +- .../Resources/config/routing.yml | 3 + .../Resources/config/services.yml | 8 + .../Resources/public/js/translator.js | 73 +++ .../views/Translation/translation.js.twig | 1 + .../Tests/Unit/Controller/ControllerTest.php | 152 +++++ .../DependencyInjection/ConfigurationTest.php | 59 ++ .../OroTranslationExtensionTest.php | 33 +- .../Tests/Unit/Form/Type/Stub/TestEntity.php | 2 +- .../Form/Type/TranslatableEntityTypeTest.php | 11 +- .../Tests/Unit/Translation/TranslatorTest.php | 239 ++++++++ .../Translation/Translator.php | 55 ++ .../UIBundle/Resources/config/assets.yml | 8 +- .../UIBundle/Resources/config/navigation.yml | 11 - .../UIBundle/Resources/config/services.yml | 6 + .../UIBundle/Resources/public/css/all.css | 9 +- .../UIBundle/Resources/public/css/ie.css | 5 +- .../Resources/public/css/less/oro.less | 430 ++++++++++++-- .../Resources/public/img/bg-header.png | Bin 0 -> 926 bytes .../Resources/public/img/bg-search-light.png | Bin 0 -> 933 bytes .../Resources/public/img/general-sprite.png | Bin 6763 -> 10730 bytes .../Resources/public/img/home-logo.png | Bin 0 -> 1258 bytes .../public/js/backbone/bootstrap-modal.js | 6 +- .../Resources/public/js/form_buttons.js | 7 + .../UIBundle/Resources/public/js/layout.js | 4 + .../Resources/public/js/remove.confirm.js | 4 +- .../public/lib/jquery/select2.min.js | 6 +- .../Resources/translations/jsmessages.en.yml | 2 + .../Resources/views/Default/index.html.twig | 36 +- .../Resources/views/Default/logo.html.twig | 2 +- .../Resources/views/Form/fields.html.twig | 6 + .../views/Form/translateable.html.twig | 57 ++ .../Resources/views/actions/update.html.twig | 9 +- .../Resources/views/actions/view.html.twig | 8 +- .../UIBundle/Resources/views/macros.html.twig | 24 +- src/Oro/Bundle/UIBundle/Route/Router.php | 67 +++ .../UIBundle/Tests/Unit/Route/RouterTest.php | 81 +++ .../UIBundle/Twig/Node/PlaceholderNode.php | 3 +- .../Bundle/UserBundle/Acl/AclManipulator.php | 2 - .../UserBundle/Controller/GroupController.php | 11 +- .../UserBundle/Controller/RoleController.php | 10 +- .../UserBundle/Controller/UserController.php | 21 +- .../Datagrid/UserRelationDatagridManager.php | 5 +- src/Oro/Bundle/UserBundle/Entity/Group.php | 8 + src/Oro/Bundle/UserBundle/Entity/User.php | 73 ++- .../UserBundle/Form/Handler/UserHandler.php | 42 ++ .../Form/Type/UserMultiSelectType.php | 68 +++ .../Bundle/UserBundle/Form/Type/UserType.php | 12 +- .../Resources/config/navigation.yml | 34 +- .../UserBundle/Resources/config/services.yml | 17 +- .../UserBundle/Resources/public/js/status.js | 4 +- .../Resources/translations/config.en.yml | 2 +- .../Resources/translations/jsmessages.en.yml | 1 + .../Resources/views/Form/fields.html.twig | 5 + .../Resources/views/Group/update.html.twig | 3 +- .../Resources/views/Role/update.html.twig | 3 +- .../Resources/views/User/update.html.twig | 40 +- .../UserBundle/Tests/Unit/Entity/UserTest.php | 19 + .../Form/Type/UserMultiSelectTypeTest.php | 96 +++ .../Resources/public/js/views/dialog.js | 9 +- 668 files changed, 31202 insertions(+), 927 deletions(-) create mode 100644 src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php create mode 100644 src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php create mode 100644 src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml create mode 100644 src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php create mode 100644 src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php create mode 100644 src/Oro/Bundle/EmailBundle/DataFixtures/ORM/AbstractEmailFixture.php create mode 100644 src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailTemplates.php create mode 100644 src/Oro/Bundle/EmailBundle/DataFixtures/data/emails/user/update_user.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php create mode 100644 src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/EmailBundle/DependencyInjection/OroEmailExtension.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/EmailTemplateTranslation.php create mode 100644 src/Oro/Bundle/EmailBundle/Entity/Repository/EmailTemplateRepository.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/EventListener/BuildTemplateFormSubscriber.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateApiType.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateSelectType.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateTranslationType.php create mode 100644 src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php create mode 100644 src/Oro/Bundle/EmailBundle/OroEmailBundle.php create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/assets.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/navigation.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/config/validation.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/public/css/style.css create mode 100644 src/Oro/Bundle/EmailBundle/Resources/public/js/collections/templates.updater.js create mode 100644 src/Oro/Bundle/EmailBundle/Resources/public/js/models/templates.updater.js create mode 100644 src/Oro/Bundle/EmailBundle/Resources/public/js/views/templates.updater.js create mode 100644 src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/index.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Repository/EmailTemplateRepositoryTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/EventListener/BuildTemplateFormSubscriberTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Handler/EmailTemplateHandlerTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateApiTestTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateSelectTypeTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTranslationTypeTest.php create mode 100644 src/Oro/Bundle/EmailBundle/Tests/bootstrap.php create mode 100644 src/Oro/Bundle/EmailBundle/composer.json create mode 100644 src/Oro/Bundle/EmailBundle/readme.md create mode 100644 src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php create mode 100644 src/Oro/Bundle/EntityBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/EntityBundle/DependencyInjection/OroEntityExtension.php create mode 100644 src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php create mode 100644 src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php create mode 100644 src/Oro/Bundle/EntityBundle/Exception/AnnotationException.php create mode 100644 src/Oro/Bundle/EntityBundle/Exception/RuntimeException.php create mode 100644 src/Oro/Bundle/EntityBundle/Extend/ExtendManager.php create mode 100644 src/Oro/Bundle/EntityBundle/OroEntityBundle.php create mode 100644 src/Oro/Bundle/EntityBundle/README.md create mode 100644 src/Oro/Bundle/EntityBundle/Resources/config/entity_config.yml create mode 100644 src/Oro/Bundle/EntityBundle/Resources/config/form_type.yml create mode 100644 src/Oro/Bundle/EntityBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/EntityBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/EntityBundle/composer.json create mode 100644 src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/FieldConfig.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/ConfigManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagridManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditFieldDatagridManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/Events.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Exception/AnnotationException.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Exception/RuntimeException.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/EventListener/ConfigSubscriber.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Form/Type/FieldConfigTypeInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/README.md create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/form_type.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/metadata.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/navigation.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/doc/config_provider.md create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/doc/configuration.md create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/doc/index.md create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/doc/schema/entity.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/index.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/NoConfigurableEntity.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/NotFoundEntityConfigRepository.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/entity_config.yml create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigEntityTypeTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/composer.json create mode 100644 src/Oro/Bundle/EntityConfigBundle/phpunit.xml.dist create mode 100644 src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Command/CheckDynamicCommand.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Databases/DatabaseInterface.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Databases/MySQLDatabase.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Databases/PostgresDatabase.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Entity/ExtendEntityInterface.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Entity/ExtendProxyInterface.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/EventListener/DoctrineSubscriber.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Exception/RuntimeException.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Extend/ExtendManager.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Extend/ExtendRepository.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Extend/ProxyObjectFactory.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Form/Extension/ExtendEntityExtension.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Mapping/ExtendClassMetadataFactory.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Metadata/Annotation/Extend.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Metadata/Driver/AnnotationDriver.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/README.md create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/public/css/extend.css create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.apply.js create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/translations/messages.en.yml create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/views/Apply/apply.html.twig create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/unique.html.twig create mode 100644 src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig create mode 100644 src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php create mode 100644 src/Oro/Bundle/EntityExtendBundle/composer.json create mode 100644 src/Oro/Bundle/FilterBundle/Resources/translations/jsmessages.en.yml create mode 100644 src/Oro/Bundle/FormBundle/Config/BlockConfig.php create mode 100644 src/Oro/Bundle/FormBundle/Config/FormConfig.php create mode 100644 src/Oro/Bundle/FormBundle/Config/FormConfigInterface.php create mode 100644 src/Oro/Bundle/FormBundle/Config/SubBlockConfig.php create mode 100644 src/Oro/Bundle/FormBundle/Exception/RuntimeException.php create mode 100644 src/Oro/Bundle/FormBundle/Form/Extension/DataBlockExtension.php create mode 100644 src/Oro/Bundle/FormBundle/Form/Twig/DataBlocks.php create mode 100644 src/Oro/Bundle/FormBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/FormBundle/Resources/doc/reference/ui_datablock_config.md create mode 100644 src/Oro/Bundle/FormBundle/Tests/Unit/Config/BlockConfigTest.php create mode 100644 src/Oro/Bundle/FormBundle/Tests/Unit/Config/FormConfigTest.php create mode 100644 src/Oro/Bundle/FormBundle/Tests/Unit/Form/Extension/DataBlockExtensionTest.php create mode 100644 src/Oro/Bundle/FormBundle/Tests/Unit/Form/Twig/DataBlocksTest.php create mode 100644 src/Oro/Bundle/FormBundle/Twig/FormExtension.php create mode 100644 src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.en.yml create mode 100644 src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.es.yml create mode 100644 src/Oro/Bundle/ImapBundle/.gitignore create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Exception/InvalidConfigurationException.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/ImapConfig.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/ImapConnector.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/ImapConnectorFactory.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/ImapServices.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/ImapServicesFactory.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchQueryBuilder.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchStringManager.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/GmailSearchStringManager.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQuery.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryBuilder.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExpr.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprInterface.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprItem.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprNamedItemInterface.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprOperator.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValue.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValueBase.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValueInterface.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryMatch.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryValueBuilder.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManager.php create mode 100644 src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManagerInterface.php create mode 100644 src/Oro/Bundle/ImapBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/ImapBundle/DependencyInjection/OroImapExtension.php create mode 100644 src/Oro/Bundle/ImapBundle/LICENSE create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Attachment.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Body.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Content.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Exception/InvalidBodyFormatException.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/GmailImap.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Message.php create mode 100644 src/Oro/Bundle/ImapBundle/Mail/Storage/Value.php create mode 100644 src/Oro/Bundle/ImapBundle/OroImapBundle.php create mode 100644 src/Oro/Bundle/ImapBundle/README.md create mode 100644 src/Oro/Bundle/ImapBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConfigTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConnectorTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapServicesFactoryTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/GmailSearchStringManagerTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryBuilderTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprItemTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprOperatorTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprValueTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchStringManagerTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/Imap1.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/Imap2.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/SearchStringManager1.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/SearchStringManager2.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/AttachmentTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/BodyTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ContentTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ValueTest.php create mode 100644 src/Oro/Bundle/ImapBundle/Tests/bootstrap.php create mode 100644 src/Oro/Bundle/ImapBundle/composer.json create mode 100644 src/Oro/Bundle/NavigationBundle/Menu/BreadcrumbManager.php create mode 100644 src/Oro/Bundle/NavigationBundle/Resources/translations/jsmessages.en.yml create mode 100644 src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/breabcrumbs.html.twig create mode 100644 src/Oro/Bundle/NavigationBundle/Resources/views/Menu/breadcrumbs.html.twig create mode 100644 src/Oro/Bundle/NavigationBundle/Tests/Unit/Menu/BreadcrumbManagerTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Command/SpoolSendCommand.php create mode 100644 src/Oro/Bundle/NotificationBundle/Controller/Api/Rest/EmailNotificationController.php create mode 100644 src/Oro/Bundle/NotificationBundle/Controller/EmailNotificationController.php create mode 100644 src/Oro/Bundle/NotificationBundle/DataFixtures/ORM/LoadDefaultNotificationEvents.php create mode 100644 src/Oro/Bundle/NotificationBundle/Datagrid/EmailNotificationDatagridManager.php create mode 100644 src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php create mode 100644 src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/NotificationHandlerPass.php create mode 100644 src/Oro/Bundle/NotificationBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/NotificationBundle/DependencyInjection/OroNotificationExtension.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/EmailNotification.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/Event.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/RecipientList.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/Repository/EmailNotificationRepository.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/Repository/EventRepository.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/Repository/RecipientListRepository.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/Repository/SpoolItemRepository.php create mode 100644 src/Oro/Bundle/NotificationBundle/Entity/SpoolItem.php create mode 100644 src/Oro/Bundle/NotificationBundle/Event/Handler/EmailNotificationHandler.php create mode 100644 src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerAbstract.php create mode 100644 src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerInterface.php create mode 100644 src/Oro/Bundle/NotificationBundle/Event/NotificationEvent.php create mode 100644 src/Oro/Bundle/NotificationBundle/Form/Handler/EmailNotificationHandler.php create mode 100644 src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationApiType.php create mode 100644 src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationType.php create mode 100644 src/Oro/Bundle/NotificationBundle/Form/Type/RecipientListType.php create mode 100644 src/Oro/Bundle/NotificationBundle/OroNotificationBundle.php create mode 100644 src/Oro/Bundle/NotificationBundle/Provider/DoctrineListener.php create mode 100644 src/Oro/Bundle/NotificationBundle/Provider/Mailer/DbSpool.php create mode 100644 src/Oro/Bundle/NotificationBundle/Provider/NotificationManager.php create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/config/navigation.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/config/validation.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/translations/datagrid.en.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/translations/messages.en.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/translations/validators.en.yml create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/entityName.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/recipientList.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/index.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/update.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Resources/views/Form/fields.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Command/SpoolSendCommandTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/NotificationHandlerPassTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/OroNotificationExtensionTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EmailNotificationTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EventTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/RecipientListTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EmailNotificationRepositoryTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EventRepositoryTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/RecipientListRepositoryTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/SpoolItemTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/Handler/EmailNotificationHandlerTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/NotificationEventTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Entity/FakeEntity.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Resources/emails/test.template.html.twig create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Handler/EmailNotificationHandlerTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationApiTypeTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationTypeTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/RecipientListTypeTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/OroNotificationBundleTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DbSpoolTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DoctrineListenerTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/NotificationManagerTest.php create mode 100644 src/Oro/Bundle/NotificationBundle/Tests/bootstrap.php create mode 100644 src/Oro/Bundle/NotificationBundle/composer.json create mode 100644 src/Oro/Bundle/NotificationBundle/readme.md create mode 100644 src/Oro/Bundle/OrganizationBundle/Controller/Api/Rest/BusinessUnitController.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Controller/Api/Soap/BusinessUnitController.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Controller/BusinessUnitController.php create mode 100644 src/Oro/Bundle/OrganizationBundle/DataFixtures/ORM/LoadOrganizationData.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitDatagridManager.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitUpdateUserDatagridManager.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitViewUserDatagridManager.php create mode 100644 src/Oro/Bundle/OrganizationBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/OrganizationBundle/DependencyInjection/OroOrganizationExtension.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Entity/Organization.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Entity/Repository/BusinessUnitRepository.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Form/Handler/BusinessUnitHandler.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitApiType.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitType.php create mode 100644 src/Oro/Bundle/OrganizationBundle/OroOrganizationBundle.php create mode 100644 src/Oro/Bundle/OrganizationBundle/README.md create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/config/datagrid.yml create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/config/navigation.yml create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/config/validation.yml create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/businessUnitsTree.html.twig create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/index.html.twig create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/update.html.twig create mode 100644 src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/view.html.twig create mode 100644 src/Oro/Bundle/OrganizationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/BusinessUnitTest.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/OrganizationTest.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Handler/BusinessUnitHandlerTest.php create mode 100644 src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Type/BusinessUnitTypeTest.php create mode 100644 src/Oro/Bundle/OrganizationBundle/composer.json create mode 100644 src/Oro/Bundle/TagBundle/Tests/Functional/ControllersTest.php create mode 100644 src/Oro/Bundle/TestFrameworkBundle/Pages/AbstractEntity.php create mode 100644 src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php create mode 100644 src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php create mode 100644 src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsTest.php create mode 100644 src/Oro/Bundle/TranslationBundle/Command/OroTranslationDumpCommand.php create mode 100644 src/Oro/Bundle/TranslationBundle/Controller/Controller.php create mode 100644 src/Oro/Bundle/TranslationBundle/DependencyInjection/Configuration.php create mode 100644 src/Oro/Bundle/TranslationBundle/Resources/config/routing.yml create mode 100644 src/Oro/Bundle/TranslationBundle/Resources/config/services.yml create mode 100644 src/Oro/Bundle/TranslationBundle/Resources/public/js/translator.js create mode 100644 src/Oro/Bundle/TranslationBundle/Resources/views/Translation/translation.js.twig create mode 100644 src/Oro/Bundle/TranslationBundle/Tests/Unit/Controller/ControllerTest.php create mode 100644 src/Oro/Bundle/TranslationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php create mode 100644 src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/TranslatorTest.php create mode 100644 src/Oro/Bundle/TranslationBundle/Translation/Translator.php create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/img/bg-header.png create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/img/bg-search-light.png create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/img/home-logo.png create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/js/form_buttons.js create mode 100644 src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml create mode 100644 src/Oro/Bundle/UIBundle/Resources/views/Form/translateable.html.twig create mode 100644 src/Oro/Bundle/UIBundle/Route/Router.php create mode 100644 src/Oro/Bundle/UIBundle/Tests/Unit/Route/RouterTest.php create mode 100644 src/Oro/Bundle/UserBundle/Form/Type/UserMultiSelectType.php create mode 100644 src/Oro/Bundle/UserBundle/Resources/translations/jsmessages.en.yml create mode 100644 src/Oro/Bundle/UserBundle/Resources/views/Form/fields.html.twig create mode 100644 src/Oro/Bundle/UserBundle/Tests/Unit/Form/Type/UserMultiSelectTypeTest.php diff --git a/composer.json b/composer.json index 211222cba65..54f00d999e3 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "leafo/lessphp": "dev-master", "willdurand/expose-translation-bundle": "0.2.*@dev", "apy/jsfv-bundle": "dev-master", - "genemu/form-bundle": "2.2.*" + "genemu/form-bundle": "2.2.*", + "a2lix/translation-form-bundle" : "1.*@dev" }, "repositories": [ { diff --git a/src/Oro/Bundle/AddressBundle/Entity/Country.php b/src/Oro/Bundle/AddressBundle/Entity/Country.php index d044a5063d1..06a98881901 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Country.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Country.php @@ -14,7 +14,9 @@ /** * Country * - * @ORM\Table("oro_dictionary_country") + * @ORM\Table("oro_dictionary_country", indexes={ + * @ORM\Index(name="name_idx", columns={"name"}) + * }) * @ORM\Entity * @Gedmo\TranslationEntity(class="Oro\Bundle\AddressBundle\Entity\CountryTranslation") */ diff --git a/src/Oro/Bundle/AddressBundle/Entity/Region.php b/src/Oro/Bundle/AddressBundle/Entity/Region.php index 48281d07296..8a5edd5dd89 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Region.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Region.php @@ -14,7 +14,9 @@ /** * Region * - * @ORM\Table("oro_dictionary_region") + * @ORM\Table("oro_dictionary_region", indexes={ + * @ORM\Index(name="name_idx", columns={"name"}) + * }) * @ORM\Entity(repositoryClass="Oro\Bundle\AddressBundle\Entity\Repository\RegionRepository") * @Gedmo\TranslationEntity(class="Oro\Bundle\AddressBundle\Entity\RegionTranslation") */ diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php new file mode 100644 index 00000000000..364118f64b3 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php @@ -0,0 +1,66 @@ +client)) { + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + } else { + $this->client->restart(); + } + } + + /** + * @return array + */ + public function testGetAddressTypes() + { + $this->client->request( + 'GET', + $this->client->generate('oro_api_get_addresstypes') + ); + + /** @var $result Response */ + $result = $this->client->getResponse(); + + ToolsAPI::assertJsonResponse($result, 200); + $result = ToolsAPI::jsonToArray($result->getContent()); + + $this->assertNotEmpty($result); + return $result; + } + + /** + * @depends testGetAddressTypes + * @param array $expected + */ + public function testGetAddressType($expected) + { + foreach ($expected as $addrType) { + $this->client->request( + 'GET', + $this->client->generate('oro_api_get_addresstype', array('name' => $addrType['name'])) + ); + /** @var $result Response */ + $result = $this->client->getResponse(); + + ToolsAPI::assertJsonResponse($result, 200); + $result = ToolsAPI::jsonToArray($result->getContent()); + $this->assertNotEmpty($result); + $this->assertEquals($addrType, $result); + } + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php index 69fedd79062..4fe28e10a70 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php @@ -166,7 +166,7 @@ public function testGetCountries() $result = $this->client->getResponse(); ToolsAPI::assertJsonResponse($result, 200); $result = ToolsAPI::jsonToArray($result->getContent()); - return $result; + return array_slice($result, 0, 5); } /** @@ -175,7 +175,6 @@ public function testGetCountries() */ public function testGetCountry($countries) { - $i = 0; foreach ($countries as $country) { $this->client->request( 'GET', @@ -186,10 +185,6 @@ public function testGetCountry($countries) ToolsAPI::assertJsonResponse($result, 200); $result = ToolsAPI::jsonToArray($result->getContent()); $this->assertEquals($country, $result); - $i++; - if ($i % 5 == 0) { - break; - } } } @@ -207,7 +202,7 @@ public function testGetRegions() $result = $this->client->getResponse(); ToolsAPI::assertJsonResponse($result, 200); $result = ToolsAPI::jsonToArray($result->getContent()); - return $result; + return array_slice($result, 0, 5); } /** @@ -216,7 +211,6 @@ public function testGetRegions() */ public function testGetRegion($regions) { - $i = 0; foreach ($regions as $region) { $this->client->request( 'GET', @@ -228,10 +222,6 @@ public function testGetRegion($regions) ToolsAPI::assertJsonResponse($result, 200); $result = ToolsAPI::jsonToArray($result->getContent()); $this->assertEquals($region, $result); - $i++; - if ($i % 5 == 0) { - break; - } } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php new file mode 100644 index 00000000000..591ac0972e8 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php @@ -0,0 +1,64 @@ +client)) { + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); + } else { + $this->client->restart(); + } + + } + + /** + * @return array + */ + public function testGetAddressTypes() + { + $result = $this->client->soapClient->getAddressTypes(); + $result = ToolsAPI::classToArray($result); + if (is_array(reset($result['item']))) { + $actualData = $result['item']; + } else { + $actualData[] = $result['item']; + } + $this->assertNotEmpty($actualData); + + return $actualData; + } + + /** + * @depends testGetAddressTypes + * @param array $expected + */ + public function testGetAddressType($expected) + { + foreach ($expected as $addrType) { + $result = $this->client->soapClient->getAddressType($addrType['name']); + $result = ToolsAPI::classToArray($result); + $this->assertNotEmpty($result); + $this->assertEquals($addrType, $result); + } + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php index aab423c49c7..d51e2b69265 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php @@ -169,7 +169,7 @@ public function testGetCountries() { $result = $this->client->soapClient->getCountries(); $result = ToolsAPI::classToArray($result); - return $result['item']; + return array_slice($result['item'], 0, 5); } /** @@ -178,15 +178,10 @@ public function testGetCountries() */ public function testGetCountry($countries) { - $i = 0; foreach ($countries as $country) { $result = $this->client->soapClient->getCountry($country['iso2Code']); $result = ToolsAPI::classToArray($result); $this->assertEquals($country, $result); - $i++; - if ($i % 5 == 0) { - break; - } } } @@ -197,7 +192,7 @@ public function testGetRegions() { $result = $this->client->soapClient->getRegions(); $result = ToolsAPI::classToArray($result); - return $result['item']; + return array_slice($result['item'], 0, 5); } /** @@ -206,15 +201,10 @@ public function testGetRegions() */ public function testGetRegion($regions) { - $i = 0; foreach ($regions as $region) { $result = $this->client->soapClient->getRegion($region['combinedCode']); $result = ToolsAPI::classToArray($result); $this->assertEquals($region, $result); - $i++; - if ($i % 5 == 0) { - break; - } } } diff --git a/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php b/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php index 268fabbfdf8..a56c3758794 100644 --- a/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php +++ b/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php @@ -125,42 +125,7 @@ public function load() */ public function getLastModified(AssetInterface $asset) { - $mtime = 0; - foreach ($asset instanceof AssetCollectionInterface ? $asset : array($asset) as $leaf) { - $mtime = max($mtime, $leaf->getLastModified()); - - if (!$filters = $leaf->getFilters()) { - continue; - } - - // prepare load path - $sourceRoot = $leaf->getSourceRoot(); - $sourcePath = $leaf->getSourcePath(); - $loadPath = $sourceRoot && $sourcePath ? dirname($sourceRoot . '/' . $sourcePath) : null; - - $prevFilters = array(); - foreach ($filters as $filter) { - $prevFilters[] = $filter; - - if (!$filter instanceof DependencyExtractorInterface) { - continue; - } - - // extract children from leaf after running all preceeding filters - $clone = clone $leaf; - $clone->clearFilters(); - foreach (array_slice($prevFilters, 0, -1) as $prevFilter) { - $clone->ensureFilter($prevFilter); - } - $clone->load(); - - foreach ($filter->getChildren($this->factory, $clone->getContent(), $loadPath) as $child) { - $mtime = max($mtime, $this->getLastModified($child)); - } - } - } - - return $mtime; + return $this->am->getLastModified($asset); } /** diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php index 774ee70c047..fd35995c3e7 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php @@ -130,22 +130,10 @@ public function testGetFormula() public function testGetLastModified() { $asset = $this->getMock('Assetic\Asset\AssetInterface'); - $child = $this->getMock('Assetic\Asset\AssetInterface'); - $filter1 = $this->getMock('Assetic\Filter\FilterInterface'); - $filter2 = $this->getMock('Assetic\Filter\DependencyExtractorInterface'); - $asset->expects($this->any()) + $this->am->expects($this->any()) ->method('getLastModified') ->will($this->returnValue(123)); - $asset->expects($this->any()) - ->method('getFilters') - ->will($this->returnValue(array($filter1, $filter2))); - $child->expects($this->any()) - ->method('getLastModified') - ->will($this->returnValue(456)); - $child->expects($this->any()) - ->method('getFilters') - ->will($this->returnValue(array())); $this->assertEquals(123, $this->manager->getLastModified($asset)); } diff --git a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php index 7ed761dfb3b..ad5f8aeda0d 100644 --- a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php +++ b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php @@ -10,6 +10,7 @@ use Oro\Bundle\DataAuditBundle\Loggable\LoggableManager; use Oro\Bundle\DataAuditBundle\Metadata\ExtendMetadataFactory; +use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; class EntitySubscriber implements EventSubscriber { @@ -26,6 +27,7 @@ class EntitySubscriber implements EventSubscriber /** * @param LoggableManager $loggableManager * @param ExtendMetadataFactory $metadataFactory + * @param ConfigProvider $auditConfigProvider */ public function __construct(LoggableManager $loggableManager, ExtendMetadataFactory $metadataFactory) { @@ -58,7 +60,9 @@ public function onFlush(OnFlushEventArgs $event) */ public function loadClassMetadata(LoadClassMetadataEventArgs $event) { - if ($metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata())) { + if ($event->getClassMetadata()->getReflectionClass() + && $metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata()) + ) { $this->loggableManager->addConfig($metadata); } } diff --git a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php index 150bc81da97..4589a1f1a29 100644 --- a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php +++ b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php @@ -1,6 +1,8 @@ logEntityClass = $logEntityClass; + $this->auditConfigProvider = $auditConfigProvider; + $this->logEntityClass = $logEntityClass; } /** @@ -153,7 +162,7 @@ public function handleLoggable(EntityManager $em) } /** - * @param $entity + * @param $entity * @param EntityManager $em */ public function handlePostPersist($entity, EntityManager $em) @@ -237,7 +246,7 @@ function ($result, $item) use ($method) { /** * @param string $action - * @param mixed $entity + * @param mixed $entity * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -248,6 +257,8 @@ protected function createLogEntity($action, $entity) return; } + $this->checkAuditable(get_class($entity)); + /** @var User $user */ $user = $this->em->getRepository('OroUserBundle:User')->findOneBy(array('username' => $this->username)); @@ -324,8 +335,8 @@ protected function createLogEntity($action, $entity) } $method = $meta->propertyMetadata[$field]->method; - $old = ($old !== null) ? $old->$method() : $old; - $new = ($new !== null) ? $new->$method() : $new; + $old = ($old !== null) ? $old->$method() : $old; + $new = ($new !== null) ? $new->$method() : $new; } $newValues[$field] = array( @@ -371,7 +382,6 @@ protected function createLogEntity($action, $entity) /** * Get the LogEntry class - * * @return string */ protected function getLogEntityClass() @@ -381,7 +391,6 @@ protected function getLogEntityClass() /** * Add flexible attribute log to a parent entity's log entry - * * @param AbstractEntityFlexibleValue $entity * @return boolean True if value has been saved, false otherwise */ @@ -495,4 +504,34 @@ protected function getIdentifier($entity, $entityMeta = null) return $entityMeta->getReflectionProperty($identifierField)->getValue($entity); } + + protected function checkAuditable($entityClassName) + { + if ($this->hasConfig($entityClassName)) { + return; + } + + if ($this->auditConfigProvider->hasConfig($entityClassName) + && $this->auditConfigProvider->getConfig($entityClassName)->is('auditable') + ) { + $reflection = new \ReflectionClass($entityClassName); + $classMetadata = new ClassMetadata($reflection->getName()); + + foreach ($reflection->getProperties() as $reflectionProperty) { + if ($this->auditConfigProvider->hasFieldConfig($entityClassName, $reflectionProperty->getName()) + && ($fieldConfig = $this->auditConfigProvider->getFieldConfig($entityClassName, $reflectionProperty->getName())) + && $fieldConfig->is('auditable') + ) { + $propertyMetadata = new PropertyMetadata($entityClassName, $reflectionProperty->getName()); + $propertyMetadata->method = '__toString'; + + $classMetadata->addPropertyMetadata($propertyMetadata); + } + } + + if (count($classMetadata->propertyMetadata)) { + $this->addConfig($classMetadata); + } + } + } } diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml new file mode 100644 index 00000000000..ea4e62daa51 --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml @@ -0,0 +1,44 @@ +oro_entity_config: + dataaudit: + entity: + items: + auditable: + options: + priority: 60 + is_bool: true + grid: + type: boolean + label: 'Auditable' + filter_type: oro_grid_orm_boolean + required: true + sortable: true + filterable: true + show_filter: false + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: other + label: 'Auditable' + field: + items: + auditable: + options: + priority: 60 + is_bool: true + grid: + type: boolean + label: 'Auditable' + filter_type: oro_grid_orm_boolean + required: true + sortable: true + filterable: false + show_filter: false + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: other + label: 'Auditable' diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/navigation.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/navigation.yml index 012c184991e..15d25df4886 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/navigation.yml @@ -3,6 +3,8 @@ oro_menu_config: audit_list: label: 'Data Audit' route: 'oro_dataaudit_index' + extras: + routes: ['oro_dataaudit_*'] tree: application_menu: @@ -12,4 +14,4 @@ oro_menu_config: audit_list: ~ oro_titles: - oro_dataaudit_index: Data Audit + oro_dataaudit_index: ~ diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml index 486e63ae875..2402392a66b 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml @@ -12,7 +12,9 @@ parameters: services: oro_dataaudit.loggable.loggable_manager: class: %oro_dataaudit.loggable.loggable_manager.class% - arguments: [%oro_dataaudit.loggable.entity.class%] + arguments: + - %oro_dataaudit.loggable.entity.class% + - @oro_dataaudit.config.config_provider oro_dataaudit.metadata.metadata_factory: class: %oro_dataaudit.metadata.metadata_factory.class% @@ -26,7 +28,9 @@ services: oro_dataaudit.listener.entity_subscriber: class: %oro_dataaudit.listener.entity_subscriber.class% - arguments: [@oro_dataaudit.loggable.loggable_manager, @oro_dataaudit.metadata.metadata_factory] + arguments: + - @oro_dataaudit.loggable.loggable_manager + - @oro_dataaudit.metadata.metadata_factory tags: - { name: doctrine.event_subscriber, connection: default } @@ -35,3 +39,7 @@ services: arguments: [@oro_dataaudit.loggable.loggable_manager, @security.context] tags: - { name: kernel.event_listener, event: kernel.request , method: onKernelRequest} + + oro_dataaudit.config.config_provider: + tags: + - { name: oro_entity_config.provider, scope: dataaudit } diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig b/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig index bfb3cf326f5..5f27f7b5b34 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig +++ b/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig @@ -1,6 +1,6 @@ diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php index 86442fe3203..9f22183b670 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php @@ -42,7 +42,7 @@ public function testPreconditions() $result = $this->client->soapClient->getAudits(); $result = ToolsAPI::classToArray($result); if (!empty($result)) { - if (!is_array(array_shift($result['item']))) { + if (!is_array(reset($result['item']))) { $result[] = $result['item']; unset($result['item']); } else { diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php index f3d510b7749..9ea42160961 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php @@ -40,7 +40,16 @@ public function setUp() $this->em->getClassMetadata('Oro\Bundle\DataAuditBundle\Tests\Unit\Fixture\LoggableClass') ); - $this->loggableManager = new LoggableManager('Oro\Bundle\DataAuditBundle\Entity\Audit'); + $provider = $this->getMockBuilder('\Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') + ->disableOriginalConstructor() + ->getMock(); + + $provider + ->expects($this->any()) + ->method('hasConfig') + ->will($this->returnValue(false)); + + $this->loggableManager = new LoggableManager('Oro\Bundle\DataAuditBundle\Entity\Audit', $provider); $this->loggableManager->addConfig($this->config); $this->loggableClass = new LoggableClass(); diff --git a/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php new file mode 100644 index 00000000000..d86c546bc4b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php @@ -0,0 +1,121 @@ +getManager()->find($id); + if (!$entity) { + return $this->handleView($this->view(null, Codes::HTTP_NOT_FOUND)); + } + + /** + * Deny to remove system templates + * + * @TODO hide icon in datagrid when it'll be possible + */ + if ($entity->getIsSystem()) { + return $this->handleView($this->view(null, Codes::HTTP_FORBIDDEN)); + } + + $em = $this->getManager()->getObjectManager(); + $em->remove($entity); + $em->flush(); + + return $this->handleView($this->view(null, Codes::HTTP_NO_CONTENT)); + } + + /** + * REST GET templates by entity name + * + * @param string $entityName + * + * @ApiDoc( + * description="Get templates by entity name", + * resource=true + * ) + * @AclAncestor("oro_email_emailtemplate_index") + * @return Response + */ + public function getAction($entityName = null) + { + if (!$entityName) { + return $this->handleView( + $this->view(null, Codes::HTTP_NOT_FOUND) + ); + } + $entityName = str_replace('_', '\\', $entityName); + + /** @var $emailTemplateRepository EmailTemplateRepository */ + $emailTemplateRepository = $this->getDoctrine()->getRepository('OroEmailBundle:EmailTemplate'); + $templates = $emailTemplateRepository->getTemplateByEntityName($entityName); + + return $this->handleView( + $this->view($templates, Codes::HTTP_OK) + ); + } + + /** + * Get entity Manager + * + * @return ApiEntityManager + */ + public function getManager() + { + return $this->get('oro_email.manager.emailtemplate.api'); + } + + /** + * @return FormInterface + */ + public function getForm() + { + return $this->get('oro_email.form.type.emailtemplate.api'); + } + + /** + * @return ApiFormHandler + */ + public function getFormHandler() + { + return $this->get('oro_email.form.handler.emailtemplate.api'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php b/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php new file mode 100644 index 00000000000..b9414cf2c07 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php @@ -0,0 +1,116 @@ +get('oro_email.emailtemplate.datagrid_manager'); + $datagridView = $gridManager->getDatagrid()->createView(); + + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } + + return array('datagrid' => $datagridView); + } + + /** + * @Route("/update/{id}", requirements={"id"="\d+"}, defaults={"id"=0})) + * @Acl( + * id="oro_email_emailtemplate_update", + * name="Edit email template", + * description="Edit email template", + * parent="oro_email_emailtemplate" + * ) + * @Template() + */ + public function updateAction(EmailTemplate $entity, $isClone = false) + { + if ($this->get('oro_email.form.handler.emailtemplate')->process($entity)) { + $this->get('session')->getFlashBag()->add( + 'success', + $this->get('translator')->trans('oro.email.controller.emailtemplate.saved.message') + ); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_email_emailtemplate_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_email_emailtemplate_index', + ) + ); + } + + return array( + 'form' => $this->get('oro_email.form.emailtemplate')->createView(), + 'isClone' => $isClone + ); + } + + /** + * @Route("/create") + * @Acl( + * id="oro_email_emailtemplate_create", + * name="Create email template", + * description="Create email template", + * parent="oro_email_emailtemplate" + * ) + * @Template("OroEmailBundle:EmailTemplate:update.html.twig") + */ + public function createAction() + { + return $this->updateAction(new EmailTemplate()); + } + + /** + * @Route("/clone/{id}", requirements={"id"="\d+"}, defaults={"id"=0})) + * @Acl( + * id="oro_email_emailtemplate_clone", + * name="Clone email template", + * description="Clone email template", + * parent="oro_email_emailtemplate" + * ) + * @Template("OroEmailBundle:EmailTemplate:update.html.twig") + */ + public function cloneAction(EmailTemplate $entity) + { + return $this->updateAction(clone $entity, true); + } +} diff --git a/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/AbstractEmailFixture.php b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/AbstractEmailFixture.php new file mode 100644 index 00000000000..0631bcb6821 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/AbstractEmailFixture.php @@ -0,0 +1,80 @@ +getEmailTemplatesList($this->getEmailsDir()); + + foreach ($emailTemplates as $fileName => $file) { + $template = file_get_contents($file['path']); + $emailTemplate = new EmailTemplate($fileName, $template, $file['format']); + $manager->persist($emailTemplate); + } + + $manager->flush(); + } + + /** + * @param string $dir + * @return array + */ + public function getEmailTemplatesList($dir) + { + if (is_dir($dir)) { + $finder = new Finder(); + $files = $finder->files()->in($dir); + } else { + $files = array(); + } + + $templates = array(); + /** @var \Symfony\Component\Finder\SplFileInfo $file */ + foreach ($files as $file) { + $fileName = str_replace(array('.html.twig', '.html', '.txt.twig', '.txt'), '', $file->getFilename()); + if (preg_match('#/([\w]+Bundle)/#', $file->getPath(), $match)) { + $fileName = $match[1] . ':' . $fileName; + } + + $format = 'html'; + if (preg_match('#\.(html|txt)(\.twig)?#', $file->getFilename(), $match)) { + $format = $match[1]; + } + + $templates[$fileName] = array( + 'path' => $file->getPath() . DIRECTORY_SEPARATOR . $file->getFilename(), + 'format' => $format, + ); + } + + return $templates; + } + + /** + * Return path to email templates + * + * @return string + */ + abstract public function getEmailsDir(); + + /** + * {@inheritDoc} + */ + public function getOrder() + { + return 120; + } +} diff --git a/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailTemplates.php b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailTemplates.php new file mode 100644 index 00000000000..fe7db990975 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailTemplates.php @@ -0,0 +1,16 @@ +Some dude updated user '{{ entity.username }}' \ No newline at end of file diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php new file mode 100644 index 00000000000..87d2a23dfab --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php @@ -0,0 +1,201 @@ +entityNameChoice = array_map( + function ($value) { + return isset($value['name'])? $value['name'] : ''; + }, + $entitiesConfig + ); + } + + /** + * {@inheritDoc} + */ + protected function getProperties() + { + return array( + new UrlProperty('update_link', $this->router, 'oro_email_emailtemplate_update', array('id')), + new UrlProperty('clone_link', $this->router, 'oro_email_emailtemplate_clone', array('id')), + new UrlProperty('delete_link', $this->router, 'oro_api_delete_emailtemplate', array('id')), + ); + } + + /** + * {@inheritDoc} + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $fieldId = new FieldDescription(); + $fieldId->setName('id'); + $fieldId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => $this->translate('ID'), + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'show_column' => false + ) + ); + $fieldsCollection->add($fieldId); + /*----------------------------------------------------------------*/ + + $fieldEntityName = new FieldDescription(); + $fieldEntityName->setName('entityName'); + $fieldEntityName->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => $this->translate('oro.email.datagrid.emailtemplate.column.entity_name'), + 'field_name' => 'entityName', + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'choices' => $this->entityNameChoice, + 'translation_domain' => 'config', + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $templateDataProperty = new TwigTemplateProperty( + $fieldEntityName, + 'OroNotificationBundle:EmailNotification:Datagrid/Property/entityName.html.twig' + ); + $fieldEntityName->setProperty($templateDataProperty); + $fieldsCollection->add($fieldEntityName); + /*----------------------------------------------------------------*/ + + $fieldName = new FieldDescription(); + $fieldName->setName('name'); + $fieldName->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => $this->translate('oro.email.datagrid.emailtemplate.column.name'), + 'field_name' => 'name', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldName); + /*----------------------------------------------------------------*/ + + $fieldType = new FieldDescription(); + $fieldType->setName('type'); + $fieldType->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_OPTIONS, + 'label' => $this->translate('oro.email.datagrid.emailtemplate.column.type'), + 'field_name' => 'type', + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + 'choices' => array( + 'html' => $this->translate('oro.email.datagrid.emailtemplate.filter.type.html'), + 'txt' => $this->translate('oro.email.datagrid.emailtemplate.filter.type.txt'), + ), + ) + ); + $fieldsCollection->add($fieldType); + /*----------------------------------------------------------------*/ + + $fieldIsSystem = new FieldDescription(); + $fieldIsSystem->setName('isSystem'); + $fieldIsSystem->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_OPTIONS, + 'field_name' => 'isSystem', + 'label' => $this->translate('oro.email.datagrid.emailtemplate.column.isSystem'), + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'choices' => array( + 0 => $this->translate('oro.email.datagrid.emailtemplate.filter.isSystem.no'), + 1 => $this->translate('oro.email.datagrid.emailtemplate.filter.isSystem.yes'), + ), + ) + ); + $fieldsCollection->add($fieldIsSystem); + } + + /** + * {@inheritDoc} + */ + protected function getRowActions() + { + $clickAction = array( + 'name' => 'rowClick', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_email_emailtemplate_update', + 'options' => array( + 'label' => $this->translate('oro.email.datagrid.emailtemplate.action.update'), + 'link' => 'update_link', + 'runOnRowClick' => true, + ) + ); + + $updateAction = array( + 'name' => 'update', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_email_emailtemplate_update', + 'options' => array( + 'label' => $this->translate('oro.email.datagrid.emailtemplate.action.update'), + 'icon' => 'edit', + 'link' => 'update_link', + ) + ); + + /** + * @TODO hide icon in datagrid when it'll be possible for non system templates and delete icon for another one + */ + $cloneAction = array( + 'name' => 'clone', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_email_emailtemplate_clone', + 'options' => array( + 'label' => $this->translate('oro.email.datagrid.emailtemplate.action.clone'), + 'icon' => 'share', + 'link' => 'clone_link', + ) + ); + + $deleteAction = array( + 'name' => 'delete', + 'type' => ActionInterface::TYPE_DELETE, + 'acl_resource' => 'oro_email_emailtemplate_remove', + 'options' => array( + 'label' => $this->translate('oro.email.datagrid.emailtemplate.action.delete'), + 'icon' => 'trash', + 'link' => 'delete_link', + ) + ); + + return array($clickAction, $updateAction, $cloneAction, $deleteAction); + } +} diff --git a/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..f220b2a8400 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php @@ -0,0 +1,26 @@ +root('oro_email'); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/EmailBundle/DependencyInjection/OroEmailExtension.php b/src/Oro/Bundle/EmailBundle/DependencyInjection/OroEmailExtension.php new file mode 100644 index 00000000000..751b6229c65 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DependencyInjection/OroEmailExtension.php @@ -0,0 +1,29 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + $loader->load('datagrid.yml'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php new file mode 100644 index 00000000000..7433befd921 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php @@ -0,0 +1,370 @@ +isSystem = $isSystem; + + foreach (array('subject', 'entityName', 'isSystem') as $templateParam) { + if (preg_match('#@' . $templateParam . '\s?=\s?(.*)\n#i', $content, $match)) { + $this->$templateParam = trim($match[1]); + $content = trim(str_replace($match[0], '', $content)); + } + } + + $this->type = $type; + $this->name = $name; + $this->content = $content; + $this->translations = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set name + * + * @param string $name + * @return EmailTemplate + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set parent + * + * @param integer $parent + * @return EmailTemplate + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Get parent + * + * @return integer + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set subject + * + * @param string $subject + * @return EmailTemplate + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Get subject + * + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Set content + * + * @param string $content + * @return EmailTemplate + */ + public function setContent($content) + { + $this->content = $content; + + return $this; + } + + /** + * Get content + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Set entityName + * + * @param string $entityName + * @return EmailTemplate + */ + public function setEntityName($entityName) + { + $this->entityName = $entityName; + + return $this; + } + + /** + * Get entityName + * + * @return string + */ + public function getEntityName() + { + return $this->entityName; + } + + /** + * Set is template system + * + * @param boolean $isSystem + * @return EmailTemplate + */ + public function setIsSystem($isSystem) + { + $this->isSystem = $isSystem; + + return $this; + } + + /** + * Get is template system + * + * @return boolean + */ + public function getIsSystem() + { + return $this->isSystem; + } + + /** + * Set locale + * + * @param mixed $locale + * @return EmailTemplate + */ + public function setLocale($locale) + { + $this->locale = $locale; + + return $this; + } + + /** + * Get locale + * + * @return mixed + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Set template type + * + * @param string $type + * @return EmailTemplate + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get template type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set translations + * + * @param ArrayCollection $translations + * @return EmailTemplate + */ + public function setTranslations($translations) + { + /** @var EmailTemplateTranslation $translation */ + foreach ($translations as $translation) { + $translation->setObject($this); + } + + $this->translations = $translations; + return $this; + } + + /** + * Get translations + * + * @return ArrayCollection + */ + public function getTranslations() + { + return $this->translations; + } + + /** + * Clone template + */ + public function __clone() + { + // cloned entity will be child + $this->parent = $this->id; + $this->id = null; + $this->isSystem = false; + } + + /** + * Convert entity to string + * + * @return string + */ + public function __toString() + { + return $this->getName(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailTemplateTranslation.php b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplateTranslation.php new file mode 100644 index 00000000000..55af87fc1cb --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplateTranslation.php @@ -0,0 +1,21 @@ +findBy(array('entityName' => $entityName)); + } + + /** + * Return templates query builder filtered by entity name + * + * @param $entityName + * @return \Doctrine\ORM\QueryBuilder + */ + public function getEntityTemplatesQueryBuilder($entityName) + { + return $this->createQueryBuilder('e') + ->where('e.entityName = :entityName') + ->orderBy('e.name', 'ASC') + ->setParameter('entityName', $entityName); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/EventListener/BuildTemplateFormSubscriber.php b/src/Oro/Bundle/EmailBundle/Form/EventListener/BuildTemplateFormSubscriber.php new file mode 100644 index 00000000000..e6879ae3180 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/EventListener/BuildTemplateFormSubscriber.php @@ -0,0 +1,139 @@ +em = $em; + $this->factory = $factory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SUBMIT => 'preSubmit' + ); + } + + /** + * Removes or adds a template field based on the entity set. + * + * @param FormEvent $event + */ + public function preSetData(FormEvent $event) + { + $notification = $event->getData(); + $form = $event->getForm(); + + if (null === $notification) { + return; + } + + $entityName = $notification->getEntityName(); + + if ($entityName) { + if ($form->has('template')) { + $config = $form->get('template')->getConfig()->getOptions(); + unset($config['choice_list']); + unset($config['choices']); + } else { + $config = array(); + } + + $config['selectedEntity'] = $entityName; + $config['query_builder'] = $this->getTemplateClosure($entityName); + + if (array_key_exists('auto_initialize', $config)) { + $config['auto_initialize'] = false; + } + + $form->add( + $this->factory->createNamed( + 'template', + 'oro_email_template_list', + $notification->getTemplate(), + $config + ) + ); + } + } + + /** + * Removes or adds a template field based on the entity set on submitted form. + * + * @param FormEvent $event + */ + public function preSubmit(FormEvent $event) + { + $data = $event->getData(); + $form = $event->getForm(); + + $entityName = isset($data['entityName']) ? $data['entityName'] : false; + + if ($entityName) { + $config = $form->get('template')->getConfig()->getOptions(); + unset($config['choice_list']); + unset($config['choices']); + + $config['selectedEntity'] = $entityName; + $config['query_builder'] = $this->getTemplateClosure($entityName); + + if (array_key_exists('auto_initialize', $config)) { + $config['auto_initialize'] = false; + } + + $form->add( + $this->factory->createNamed( + 'template', + 'oro_email_template_list', + null, + $config + ) + ); + } + } + + /** + * @param string $entityName + * @return callable + */ + protected function getTemplateClosure($entityName) + { + return function (EmailTemplateRepository $templateRepository) use ($entityName) { + return $templateRepository->getEntityTemplatesQueryBuilder($entityName); + }; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php b/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php new file mode 100644 index 00000000000..c2011144844 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php @@ -0,0 +1,85 @@ +form = $form; + $this->request = $request; + $this->manager = $manager; + $this->translator = $translator; + } + + /** + * Process form + * + * @param EmailTemplate $entity + * @return bool True on successful processing, false otherwise + */ + public function process(EmailTemplate $entity) + { + $this->form->setData($entity); + + if (in_array($this->request->getMethod(), array('POST', 'PUT'))) { + // deny to modify system templates + if ($entity->getIsSystem()) { + $message = $this->translator->trans( + 'oro.mail.validators.emailtemplate.attempt_save_system_template', + array(), + 'validators' + ); + $this->form->addError(new FormError($message)); + + return false; + } + + $this->form->submit($this->request); + + if ($this->form->isValid()) { + $this->manager->persist($entity); + $this->manager->flush(); + + return true; + } + } + + return false; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateApiType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateApiType.php new file mode 100644 index 00000000000..2e7c00349a8 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateApiType.php @@ -0,0 +1,42 @@ +addEventSubscriber(new PatchSubscriber()); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\EmailBundle\Entity\EmailTemplate', + 'intention' => 'emailtemplate', + 'csrf_protection' => false, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_email_emailtemplate_api'; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateSelectType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateSelectType.php new file mode 100644 index 00000000000..e66e79d1e20 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateSelectType.php @@ -0,0 +1,67 @@ +setDefaults( + array( + 'class' => 'OroEmailBundle:EmailTemplate', + 'property' => 'name', + 'query_builder' => null, + 'depends_on_parent_field' => 'entityName', + 'selectedEntity' => null, + 'choices' => $choices, + 'configs' => array( + 'placeholder' => 'oro.email.form.choose_template', + ), + 'empty_value' => '', + 'empty_data' => null, + 'required' => true + ) + ); + } + + /** + * {@inheritdoc} + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $view->vars['depends_on_parent_field'] = $form->getConfig()->getOption('depends_on_parent_field'); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_email_template_list'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'genemu_jqueryselect2_translatable_entity'; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateTranslationType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateTranslationType.php new file mode 100644 index 00000000000..5b7330aca4c --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateTranslationType.php @@ -0,0 +1,37 @@ +setDefaults( + array( + 'translatable_class' => 'Oro\\Bundle\\EmailBundle\\Entity\\EmailTemplate', + 'intention' => 'emailtemplate_translation', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'cascade_validation' => true, + ) + ); + } + + public function getParent() + { + return 'a2lix_translations_gedmo'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_email_emailtemplate_translatation'; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php new file mode 100644 index 00000000000..d2b45def822 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php @@ -0,0 +1,106 @@ +entityNameChoices = array_map( + function ($value) { + return isset($value['name'])? $value['name'] : ''; + }, + $entitiesConfig + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add( + 'entityName', + 'choice', + array( + 'choices' => $this->entityNameChoices, + 'multiple' => false, + 'translation_domain' => 'config', + 'empty_value' => '', + 'empty_data' => null, + 'required' => true + ) + ); + + $builder->add( + 'name', + 'text', + array( + 'required' => true + ) + ); + + $builder->add( + 'type', + 'choice', + array( + 'multiple' => false, + 'expanded' => true, + 'choices' => array( + 'html' => 'oro.email.datagrid.emailtemplate.filter.type.html', + 'txt' => 'oro.email.datagrid.emailtemplate.filter.type.txt' + ), + 'required' => true, + 'translation_domain' => 'datagrid' + ) + ); + + $builder->add( + 'translations', + 'oro_email_emailtemplate_translatation', + array( + 'required' => false + ) + ); + + $builder->add( + 'parent', + 'hidden' + ); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\EmailBundle\Entity\EmailTemplate', + 'intention' => 'emailtemplate', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'cascade_validation' => true, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_email_emailtemplate'; + } +} diff --git a/src/Oro/Bundle/EmailBundle/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php new file mode 100644 index 00000000000..2e5e81d059b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php @@ -0,0 +1,9 @@ + li { + height: auto; +} +.a2lix_translations .a2lix_translationsLocales.nav.nav-tabs > .active > a, +.a2lix_translations .a2lix_translationsLocales.nav.nav-tabs > .active > a:hover, +.a2lix_translations .a2lix_translationsLocales.nav.nav-tabs > .active > a:focus { + background-color: #fff; +} +.a2lix_translationsFields.tab-content .tab-pane input, +.a2lix_translationsFields.tab-content .tab-pane textarea { + width: 700px; +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/collections/templates.updater.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/collections/templates.updater.js new file mode 100644 index 00000000000..0935aa7c6c6 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/collections/templates.updater.js @@ -0,0 +1,24 @@ +Oro = Oro || {}; +Oro.EmailTemplatesUpdater = Oro.EmailTemplatesUpdater || {}; + +Oro.EmailTemplatesUpdater.Collection = Backbone.Collection.extend({ + route: 'oro_api_get_emailtemplate', + url: null, + model: Oro.EmailTemplatesUpdater.EmailTemplate, + + /** + * Constructor + */ + initialize: function () { + this.url = Routing.generate(this.route, {entityName: null}); + }, + + /** + * Regenerate route for selected entity + * + * @param id {String} + */ + setEntityId: function (id) { + this.url = Routing.generate(this.route, {entityName: id}); + } +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/models/templates.updater.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/models/templates.updater.js new file mode 100644 index 00000000000..a23f99162d3 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/models/templates.updater.js @@ -0,0 +1,10 @@ +Oro = Oro || {}; +Oro.EmailTemplatesUpdater = Oro.EmailTemplatesUpdater || {}; + +Oro.EmailTemplatesUpdater.EmailTemplate = Backbone.Model.extend({ + defaults: { + entity: '', + id: '', + name: '' + } +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/views/templates.updater.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/views/templates.updater.js new file mode 100644 index 00000000000..fee00ffdb85 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/views/templates.updater.js @@ -0,0 +1,40 @@ +Oro = Oro || {}; +Oro.EmailTemplatesUpdater = Oro.EmailTemplatesUpdater || {}; + +Oro.EmailTemplatesUpdater.View = Backbone.View.extend({ + events: { + 'change': 'selectionChanged' + }, + target: null, + + /** + * Constructor + * + * @param options {Object} + */ + initialize: function (options) { + this.template = $('#emailtemplate-chooser-template').html(); + this.target = options.target; + + this.listenTo(this.collection, 'reset', this.render); + }, + + /** + * onChange event listener + * + * @param e {Object} + */ + selectionChanged: function (e) { + var entityId = $(e.currentTarget).val(); + this.collection.setEntityId(entityId.split('\\').join('_')); + this.collection.fetch(); + }, + + render: function() { + $(this.target).val('').trigger('change'); + $(this.target).find('option[value!=""]').remove(); + if (this.collection.models.length > 0) { + $(this.target).append(_.template(this.template, {entities: this.collection.models})); + } + } +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml new file mode 100644 index 00000000000..219f57115bc --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml @@ -0,0 +1,21 @@ +oro: + email: + datagrid: + emailtemplate: + action: + update: "Update" + clone: "Clone" + delete: "Delete" + column: + entity_name: "Entity name" + name: "Template name" + isSystem: "Is system" + type: "Type" + filter: + type: + html: "HTML" + txt: "Plain text" + isSystem: + yes: "Yes" + no: "No" + diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml new file mode 100644 index 00000000000..da42cb102d4 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml @@ -0,0 +1,8 @@ +oro: + email: + controller: + emailtemplate: + saved: + message: "Template sucessfully saved" + form: + choose_template: "Choose a template..." diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml new file mode 100644 index 00000000000..23b2934d501 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml @@ -0,0 +1,5 @@ +oro: + mail: + validators: + emailtemplate: + attempt_save_system_template: "Overriding of system's templates is prohibited, clone it instead." diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/index.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/index.html.twig new file mode 100644 index 00000000000..b0cdbaf41f9 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/index.html.twig @@ -0,0 +1,15 @@ +{% extends 'OroUIBundle:actions:index.html.twig' %} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% set gridId = 'email-template-grid' %} +{% block content %} + {% set pageTitle = 'Email Templates' %} + {% if resource_granted('oro_email_emailtemplate_create') %} + {% set buttons = [ + UI.addButton( + {'path' : path('oro_email_emailtemplate_create'), 'title' : 'Create template', 'label' : 'Create template'} + ) + ] + %} + {% endif %} + {{ parent() }} +{% endblock %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig new file mode 100644 index 00000000000..ca3e9897db8 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig @@ -0,0 +1,103 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with [ + 'OroUIBundle:Form:fields.html.twig', +]%} +{% set title = form.vars.value.id + ? 'Edit email template "%name%"'|trans({'%name%': form.vars.value.name}) + : isClone ? 'Clone Email Template'|trans : 'Add Email Template'|trans +%} +{% if form.vars.value.id %} + {% oro_title_set({params : {"%name%": form.vars.value.name} }) %} +{% endif %} + +{% set formAction = form.vars.value.id + ? path('oro_email_emailtemplate_update', { id: form.vars.value.id }) + : path('oro_email_emailtemplate_create') +%} + +{% block navButtons %} + {% if form.vars.value.id and resource_granted('oro_email_emailtemplate_clone') %} + {{ UI.button({ + 'path' : path('oro_email_emailtemplate_clone', {'id': form.vars.value.id}), + 'title' : 'Clone', + 'label' : 'Clone', + aClass: 'btn-success', + iClass: 'icon-share' + }) + }} + {{ UI.buttonSeparator() }} + {% endif %} + {% set cancelButton = UI.button({'path' : path('oro_email_emailtemplate_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) %} + {% if form.vars.value.isSystem == false %} + {% if form.vars.value.id and resource_granted('oro_email_emailtemplate_remove') %} + {{ + UI.deleteButton({ + 'dataUrl': path('oro_api_delete_emailtemplate', {'id': form.vars.value.id}), + 'dataRedirect': path('oro_email_emailtemplate_index'), + 'aCss': 'no-hash remove-button', + 'id': 'btn-remove-emailtemplate', + 'dataId': form.vars.value.id, + 'dataMessage': 'Are you sure you want to delete this email template?', + 'title': 'Delete email template', + 'label': 'Delete' + }) + }} + {{ UI.buttonSeparator() }} + {% endif %} + {{ cancelButton }} + + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} + {% else %} + {{ cancelButton }} + {% endif %} +{% endblock navButtons %} + +{% block pageHeader %} + {% if form.vars.value.id %} + {% set breadcrumbs = { + 'entity': form.vars.value, + 'indexPath': path('oro_email_emailtemplate_index'), + 'indexLabel': 'Email Templates', + 'entityTitle': title + } + %} + {{ parent() }} + {% else %} + {% include 'OroUIBundle::page_title_block.html.twig' %} + {% endif %} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'emailtemplate-edit' %} + + {% set dataBlocks = [{ + 'title': 'General', + 'class': 'active', + 'subblocks': [{ + 'title': '', + 'data': [ + form_row(form.entityName), + form_row(form.type), + form_row(form.name) + ] + }] + }, { + 'title': 'Template data', + 'subblocks': [{ + 'title': '', + 'data': [ + form_widget(form.translations) + ] + }] + }] + %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + } + %} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig new file mode 100644 index 00000000000..43bab78e941 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig @@ -0,0 +1,20 @@ +{% block oro_email_template_list_row %} + + + + {{ form_row(form) }} +{% endblock %} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php new file mode 100644 index 00000000000..9025546cd8f --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php @@ -0,0 +1,65 @@ +emailTemplate = new EmailTemplate('update_entity.html.twig', "@subject = sdfdsf\n abc"); + + $this->assertEquals('abc', $this->emailTemplate->getContent()); + $this->assertFalse($this->emailTemplate->getIsSystem()); + $this->assertEquals('html', $this->emailTemplate->getType()); + } + + public function tearDown() + { + unset($this->emailTemplate); + } + + /** + * Test setters, getters + */ + public function testSettersGetters() + { + foreach (array( + 'name', + 'isSystem', + 'parent', + 'subject', + 'content', + 'locale', + 'entityName', + 'type', + ) as $field) { + $this->emailTemplate->{'set'.ucfirst($field)}('abc'); + $this->assertEquals('abc', $this->emailTemplate->{'get'.ucfirst($field)}()); + + $translation = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplateTranslation'); + $this->emailTemplate->setTranslations(array($translation)); + $this->assertEquals($this->emailTemplate->getTranslations(), array($translation)); + } + } + + /** + * Test clone, toString + */ + public function testCloneAndToString() + { + $clone = clone $this->emailTemplate; + + $this->assertNull($clone->getId()); + $this->assertEquals($clone->getParent(), $this->emailTemplate->getId()); + + $this->assertEquals($this->emailTemplate->getName(), (string)$this->emailTemplate); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Repository/EmailTemplateRepositoryTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Repository/EmailTemplateRepositoryTest.php new file mode 100644 index 00000000000..0e620ea3881 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Repository/EmailTemplateRepositoryTest.php @@ -0,0 +1,92 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->repository = new EmailTemplateRepository( + $this->entityManager, + new ClassMetadata('Oro\Bundle\EmailBundle\Entity\EmailTemplate') + ); + } + + public function tearDown() + { + unset($this->entityManager); + unset($this->repository); + } + + /** + * Test setters, getters + */ + public function testGetTemplateByEntityName() + { + $persister = $this->getMockBuilder('Doctrine\ORM\Persisters\BasicEntityPersister') + ->disableOriginalConstructor() + ->getMock(); + $persister->expects($this->once()) + ->method('loadAll'); + + $uow = $this->getMockBuilder('\Doctrine\ORM\UnitOfWork') + ->disableOriginalConstructor() + ->getMock(); + + $uow->expects($this->once()) + ->method('getEntityPersister') + ->with('Oro\Bundle\EmailBundle\Entity\EmailTemplate') + ->will($this->returnValue($persister)); + + $this->entityManager->expects($this->once()) + ->method('getUnitOfWork') + ->will($this->returnValue($uow)); + + $this->repository->getTemplateByEntityName('Oro\Bundle\UserBundle\Entity\User'); + } + + public function testGetEntityQueryBuilder() + { + $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb->expects($this->once()) + ->method('select') + ->will($this->returnSelf()); + $qb->expects($this->once()) + ->method('from') + ->will($this->returnSelf()); + $qb->expects($this->once()) + ->method('where') + ->will($this->returnSelf()); + $qb->expects($this->once()) + ->method('orderBy') + ->will($this->returnSelf()); + $qb->expects($this->once()) + ->method('setParameter') + ->will($this->returnSelf()); + + $this->entityManager->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($qb)); + + $this->repository->getEntityTemplatesQueryBuilder('Oro\Bundle\UserBundle\Entity\User'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/EventListener/BuildTemplateFormSubscriberTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/EventListener/BuildTemplateFormSubscriberTest.php new file mode 100644 index 00000000000..03fd134f48d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/EventListener/BuildTemplateFormSubscriberTest.php @@ -0,0 +1,246 @@ +em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $this->formBuilder = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + + $this->listener = new BuildTemplateFormSubscriber($this->em, $this->formBuilder); + } + + public function testGetSubscribedEvents() + { + $result = $this->listener->getSubscribedEvents(); + + $this->assertInternalType('array', $result); + $this->assertArrayHasKey(FormEvents::PRE_SET_DATA, $result); + $this->assertArrayHasKey(FormEvents::PRE_SUBMIT, $result); + } + + public function testPreSetDataEmptyData() + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + + $eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue(null)); + $eventMock->expects($this->once()) + ->method('getForm'); + + $this->listener->preSetData($eventMock); + } + + public function testPreSetDataEmptyEntityName() + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + + $notificationMock = $this->getMock('Oro\Bundle\NotificationBundle\Entity\EmailNotification'); + $notificationMock->expects($this->once()) + ->method('getEntityName') + ->will($this->returnValue(null)); + + $eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue($notificationMock)); + $eventMock->expects($this->once()) + ->method('getForm'); + + $this->listener->preSetData($eventMock); + } + + public function testPreSetDataHasTemplates() + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + + $notificationMock = $this->getMock('Oro\Bundle\NotificationBundle\Entity\EmailNotification'); + $notificationMock->expects($this->once()) + ->method('getEntityName') + ->will($this->returnValue('testEntity')); + + $configMock = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + $configMock->expects($this->once()) + ->method('getOptions') + ->will($this->returnValue(array('auto_initialize' => true))); + + $fieldMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + + $formMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + $formMock->expects($this->once()) + ->method('has') + ->with($this->equalTo('template')) + ->will($this->returnValue(true)); + $formMock->expects($this->once()) + ->method('get') + ->with($this->equalTo('template')) + ->will($this->returnValue($fieldMock)); + $formMock->expects($this->once()) + ->method('add'); + + $fieldMock->expects($this->once()) + ->method('getConfig') + ->will($this->returnValue($configMock)); + + $newFieldMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + + $phpUnit = $this; + $this->formBuilder->expects($this->once()) + ->method('createNamed') + ->will( + $this->returnCallback( + function ($name, $type, $data, $config) use ($phpUnit, $newFieldMock) { + $phpUnit->assertEquals('oro_email_template_list', $type); + $phpUnit->assertEquals('template', $name); + $phpUnit->assertNull($data); + $phpUnit->assertArrayHasKey('selectedEntity', $config); + $phpUnit->assertArrayHasKey('auto_initialize', $config); + $phpUnit->assertArrayHasKey('query_builder', $config); + + return $newFieldMock; + } + ) + ); + + $eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue($notificationMock)); + $eventMock->expects($this->once()) + ->method('getForm') + ->will($this->returnValue($formMock)); + + $this->listener->preSetData($eventMock); + } + + public function testPreSetDataNoTemplates() + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + + $notificationMock = $this->getMock('Oro\Bundle\NotificationBundle\Entity\EmailNotification'); + $notificationMock->expects($this->once()) + ->method('getEntityName') + ->will($this->returnValue('testEntity')); + + $formMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + $formMock->expects($this->once()) + ->method('has') + ->with($this->equalTo('template')) + ->will($this->returnValue(false)); + $formMock->expects($this->never()) + ->method('get'); + $formMock->expects($this->once()) + ->method('add'); + + $newFieldMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->formBuilder->expects($this->once()) + ->method('createNamed') + ->will($this->returnValue($newFieldMock)); + + $eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue($notificationMock)); + $eventMock->expects($this->once()) + ->method('getForm') + ->will($this->returnValue($formMock)); + + $this->listener->preSetData($eventMock); + } + + public function testPreSubmitData() + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + + $configMock = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + $configMock->expects($this->once())->method('getOptions') + ->will($this->returnValue(array('auto_initialize' => true))); + + $fieldMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + $fieldMock->expects($this->once())->method('getConfig') + ->will($this->returnValue($configMock)); + + $formMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + $formMock->expects($this->once()) + ->method('get') + ->with($this->equalTo('template')) + ->will($this->returnValue($fieldMock)); + $formMock->expects($this->once()) + ->method('add'); + + $newFieldMock = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface') + ->disableOriginalConstructor() + ->getMock(); + + $phpUnit = $this; + $this->formBuilder->expects($this->once())->method('createNamed') + ->will( + $this->returnCallback( + function ($name, $type, $data, $config) use ($phpUnit, $newFieldMock) { + $phpUnit->assertEquals('template', $name); + $phpUnit->assertEquals('oro_email_template_list', $type); + $phpUnit->assertNull($data); + $phpUnit->assertArrayHasKey('query_builder', $config); + $phpUnit->assertArrayHasKey('selectedEntity', $config); + $phpUnit->assertArrayHasKey('auto_initialize', $config); + + return $newFieldMock; + } + ) + ); + + $eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue(array('entityName' => 'testEntityName'))); + $eventMock->expects($this->once()) + ->method('getForm') + ->will($this->returnValue($formMock)); + + $this->listener->preSubmit($eventMock); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Handler/EmailTemplateHandlerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Handler/EmailTemplateHandlerTest.php new file mode 100644 index 00000000000..0c1432a5f6a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Handler/EmailTemplateHandlerTest.php @@ -0,0 +1,139 @@ +form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + $this->request = new Request(); + $this->manager = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') + ->disableOriginalConstructor() + ->getMock(); + $this->translator = $this->getMockBuilder('Symfony\Component\Translation\Translator') + ->disableOriginalConstructor() + ->getMock(); + + $this->entity = new EmailTemplate(); + $this->handler = new EmailTemplateHandler($this->form, $this->request, $this->manager, $this->translator); + } + + public function testProcessUnsupportedRequest() + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->form->expects($this->never()) + ->method('submit'); + + $this->assertFalse($this->handler->process($this->entity)); + } + + /** + * @dataProvider supportedMethods + * @param string $method + */ + public function testProcessSupportedRequest($method) + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->request->setMethod($method); + + $this->form->expects($this->once()) + ->method('submit') + ->with($this->request); + + $this->assertFalse($this->handler->process($this->entity)); + } + + public function supportedMethods() + { + return array( + array('POST'), + array('PUT') + ); + } + + public function testProcessValidData() + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->request->setMethod('POST'); + + $this->form->expects($this->once()) + ->method('submit') + ->with($this->request); + + $this->form->expects($this->once()) + ->method('isValid') + ->will($this->returnValue(true)); + + $this->manager->expects($this->once()) + ->method('persist') + ->with($this->entity); + + $this->manager->expects($this->once()) + ->method('flush'); + + $this->assertTrue($this->handler->process($this->entity)); + } + + public function testAddingErrorToSystemEntity() + { + $this->entity->setIsSystem(true); + + $this->form->expects($this->once())->method('setData') + ->with($this->entity); + $this->form->expects($this->once())->method('addError'); + + $this->request->setMethod('POST'); + + $this->translator->expects($this->once()) + ->method('trans'); + + $this->assertFalse($this->handler->process($this->entity)); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateApiTestTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateApiTestTest.php new file mode 100644 index 00000000000..a03fce5213b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateApiTestTest.php @@ -0,0 +1,51 @@ +type = new EmailTemplateApiType(array()); + } + + public function tearDown() + { + unset($this->type); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('oro_email_emailtemplate_api', $this->type->getName()); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->once()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber')); + + $this->type->buildForm($builder, array()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateSelectTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateSelectTypeTest.php new file mode 100644 index 00000000000..e82366511c4 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateSelectTypeTest.php @@ -0,0 +1,71 @@ +type = new EmailTemplateSelectType(); + } + + public function tearDown() + { + unset($this->type); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + $this->type->setDefaultOptions($resolver); + } + + public function testGetParent() + { + $this->assertEquals('genemu_jqueryselect2_translatable_entity', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_email_template_list', $this->type->getName()); + } + + public function testFinishView() + { + $optionKey = 'testKey'; + + $formConfigMock = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + $formConfigMock->expects($this->once()) + ->method('getOption') + ->with('depends_on_parent_field') + ->will($this->returnValue($optionKey)); + + $formMock = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->setMethods(array('getConfig')) + ->getMock(); + $formMock->expects($this->once()) + ->method('getConfig') + ->will($this->returnValue($formConfigMock)); + + $formView = new FormView(); + $this->type->finishView($formView, $formMock, array()); + $this->assertArrayHasKey('depends_on_parent_field', $formView->vars); + $this->assertEquals($optionKey, $formView->vars['depends_on_parent_field']); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTest.php new file mode 100644 index 00000000000..38519ef7fb0 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTest.php @@ -0,0 +1,50 @@ +type = new EmailTemplateType(array()); + } + + public function tearDown() + { + unset($this->type); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('oro_email_emailtemplate', $this->type->getName()); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(5)) + ->method('add'); + + $this->type->buildForm($builder, array()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTranslationTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTranslationTypeTest.php new file mode 100644 index 00000000000..fc442692661 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateTranslationTypeTest.php @@ -0,0 +1,43 @@ +type = new EmailTemplateTranslationType(array()); + } + + public function tearDown() + { + unset($this->type); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('oro_email_emailtemplate_translatation', $this->type->getName()); + } + + public function testGetParent() + { + $this->assertEquals('a2lix_translations_gedmo', $this->type->getParent()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php b/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php new file mode 100644 index 00000000000..787e7534251 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/bootstrap.php @@ -0,0 +1,14 @@ +=5.3.3", + "symfony/symfony": "2.1.*", + "a2lix/translation-form-bundle" : "1.*@dev" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\EmailBundle": "" } + }, + "target-dir": "Oro/Bundle/EmailBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/readme.md b/src/Oro/Bundle/EmailBundle/readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php b/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php new file mode 100644 index 00000000000..88feb472072 --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php @@ -0,0 +1,26 @@ +configManager = $configManager; + } + + /** + * {@inheritDoc} + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + + } +} diff --git a/src/Oro/Bundle/EntityBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/EntityBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..ce1bd7c0dac --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/DependencyInjection/Configuration.php @@ -0,0 +1,28 @@ +root('oro_entity') + ->children() + ->scalarNode('cache_dir')->cannotBeEmpty()->defaultValue('%kernel.cache_dir%/oro_entity')->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/EntityBundle/DependencyInjection/OroEntityExtension.php b/src/Oro/Bundle/EntityBundle/DependencyInjection/OroEntityExtension.php new file mode 100644 index 00000000000..459afca4aed --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/DependencyInjection/OroEntityExtension.php @@ -0,0 +1,62 @@ +processConfiguration($configuration, $configs); + + $this->configCache($container, $config); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + $loader->load('form_type.yml'); + } + + /** + * @param ContainerBuilder $container + * @param $config + * @throws RuntimeException + */ + protected function configCache(ContainerBuilder $container, $config) + { + $cacheDir = $container->getParameterBag()->resolveValue($config['cache_dir']); + + $fs = new Filesystem(); + $fs->remove($cacheDir); + + if (!is_dir($cacheDir)) { + if (false === @mkdir($cacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); + } + } + $container->setParameter('oro_entity.cache_dir', $cacheDir); + + $auditCacheDir = $cacheDir . '/audit'; + if (!is_dir($auditCacheDir)) { + if (false === @mkdir($auditCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create "audit" cache directory "%s".', $auditCacheDir)); + } + } + $container->setParameter('oro_entity.cache_dir.audit', $auditCacheDir); + } +} diff --git a/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php b/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php new file mode 100644 index 00000000000..64683a836a5 --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php @@ -0,0 +1,160 @@ +id; + } + + /** + * @param PersistentCollection|AuditDiff[] $diffs + * @return $this + */ + public function setDiffs($diffs) + { + $this->diffs = $diffs; + + return $this; + } + + /** + * @param AuditDiff $diff + * @return $this + */ + public function addDiff(AuditDiff $diff) + { + if (!$this->diffs->contains($diff)) { + $this->diffs->add($diff); + } + + return $this; + } + + /** + * @param AuditDiff $diff + * @return $this + */ + public function removeDiff(AuditDiff $diff) + { + if ($this->diffs->contains($diff)) { + $this->diffs->remove($diff); + } + + return $this; + } + + /** + * @return PersistentCollection|AuditDiff[] + */ + public function getDiffs() + { + return $this->diffs; + } + + /** + * @param \DateTime $logged + * @return $this + */ + public function setLogged($logged) + { + $this->logged = $logged; + + return $this; + } + + /** + * @return \DateTime + */ + public function getLogged() + { + return $this->logged; + } + + /** + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param \Oro\Bundle\UserBundle\Entity\User $user + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * @return \Oro\Bundle\UserBundle\Entity\User + */ + public function getUser() + { + return $this->user; + } +} diff --git a/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php b/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php new file mode 100644 index 00000000000..30a59a72485 --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php @@ -0,0 +1,152 @@ +id; + } + + /** + * @param string $action + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + + return $this; + } + + /** + * @return string + */ + public function getAction() + { + return $this->action; + } + + /** + * @param AuditCommit $commit + * @return $this + */ + public function setCommit(AuditCommit $commit) + { + $this->commit = $commit; + + return $this; + } + + /** + * @return AuditCommit + */ + public function getCommit() + { + return $this->commit; + } + + /** + * @param string $diff + * @return $this + */ + public function setDiff($diff) + { + $this->diff = $diff; + + return $this; + } + + /** + * @return string + */ + public function getDiff() + { + return $this->diff; + } + + /** + * @param string $className + */ + public function setClassName($className) + { + $this->className = $className; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * @param int $objectId + */ + public function setObjectId($objectId) + { + $this->objectId = $objectId; + } + + /** + * @return int + */ + public function getObjectId() + { + return $this->objectId; + } +} diff --git a/src/Oro/Bundle/EntityBundle/Exception/AnnotationException.php b/src/Oro/Bundle/EntityBundle/Exception/AnnotationException.php new file mode 100644 index 00000000000..85629d2c957 --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Exception/AnnotationException.php @@ -0,0 +1,7 @@ +getMetadataDriverImpl()) { + throw ORMException::missingMappingDriverImpl(); + } + + if (is_array($conn)) { + $conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, ($eventManager ? : new EventManager())); + } elseif ($conn instanceof Connection) { + if ($eventManager !== null && $conn->getEventManager() !== $eventManager) { + throw ORMException::mismatchedEventManager(); + } + } else { + throw new \InvalidArgumentException("Invalid argument: " . $conn); + } + + return new ExtendManager($conn, $config, $conn->getEventManager()); + } +} diff --git a/src/Oro/Bundle/EntityBundle/OroEntityBundle.php b/src/Oro/Bundle/EntityBundle/OroEntityBundle.php new file mode 100644 index 00000000000..631881fe1d0 --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/OroEntityBundle.php @@ -0,0 +1,9 @@ +=5.3.3", + "symfony/symfony": "2.3.*", + "oro/ui-bundle": "dev-master", + "oro/grid-bundle": "dev-master", + "oro/navigation-bundle": "dev-master", + "oro/data-audit-bundle": "dev-master", + "oro/form-bundle": "dev-master", + "oro/user-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\EntityBundle": "" } + }, + "target-dir": "Oro/Bundle/EntityBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php new file mode 100644 index 00000000000..cc630384793 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php @@ -0,0 +1,102 @@ +configManager = $configManager; + $this->security = $security; + } + + /** + * Log all changed Config + */ + public function log() + { + if (!$this->getUser()) { + return; + } + + $log = new ConfigLog(); + $log->setUser($this->getUser()); + + foreach (array_merge($this->configManager->getUpdatedEntityConfig(), $this->configManager->getUpdatedFieldConfig()) as $config) { + $this->logConfig($config, $log); + } + + if ($log->getDiffs()->count()) { + $this->configManager->em()->persist($log); + } + } + + /** + * @param ConfigInterface $config + * @param ConfigLog $log + */ + protected function logConfig(ConfigInterface $config, ConfigLog $log) + { + $changes = $this->configManager->getConfigChangeSet($config); + + $configContainer = $this->configManager->getProvider($config->getScope())->getConfigContainer(); + if ($config instanceof FieldConfigInterface) { + $internalValues = $configContainer->getFieldInternalValues(); + } else { + $internalValues = $configContainer->getEntityInternalValues(); + } + + $changes = array_diff_key($changes, $internalValues); + + if (!count($changes)) { + return; + } + + $diff = new ConfigLogDiff(); + $diff->setScope($config->getScope()); + $diff->setDiff($changes); + $diff->setClassName($config->getClassName()); + + if ($config instanceof FieldConfigInterface) { + $diff->setFieldName($config->getCode()); + } + + $log->addDiff($diff); + } + + /** + * @return UserInterface + */ + protected function getUser() + { + if (!$this->security->getService()->getToken() || !$this->security->getService()->getToken()->getUser()) { + return false; + } + + return $this->security->getService()->getToken()->getUser(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php new file mode 100644 index 00000000000..3a45b4ea511 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php @@ -0,0 +1,26 @@ +dir = rtrim($dir, '\\/'); + } + + /** + * @param $className + * @param $scope + * @return EntityConfig + */ + public function loadConfigFromCache($className, $scope) + { + $path = $this->dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; + if (!file_exists($path)) { + return null; + } + + return include $path; + } + + /** + * @param EntityConfig $config + */ + public function putConfigInCache(EntityConfig $config) + { + $path = $this->dir . '/' . strtr($config->getClassName(), '\\', '-') . '.' . $config->getScope() . '.cache.php'; + file_put_contents($path, 'dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; + if (file_exists($path)) { + unlink($path); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php b/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php new file mode 100644 index 00000000000..9f66b761a92 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php @@ -0,0 +1,18 @@ +getContainer()->get('oro_entity_config.config_manager'); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php new file mode 100644 index 00000000000..7a52c5f36e7 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php @@ -0,0 +1,38 @@ +setName('oro:entity-config:update') + ->setDescription('EntityConfig configurator updater'); + } + + /** + * Runs command + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln($this->getDescription()); + + foreach ($this->getConfigManager()->em()->getMetadataFactory()->getAllMetadata() as $doctrineMetadata) { + $this->getConfigManager()->initConfigByDoctrineMetadata($doctrineMetadata); + } + + $this->getConfigManager()->flush(); + + $output->writeln('Completed'); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php new file mode 100644 index 00000000000..abd51c44fe5 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php @@ -0,0 +1,93 @@ +values[$code])) { + return $this->values[$code]; + } elseif ($strict) { + throw new RuntimeException(sprintf( + "Config '%s' for class '%s' in scope '%s' is not found ", + $code, $this->getClassName(), $this->getScope() + )); + } + + return null; + } + + /** + * @param $code + * @param $value + * @return string + */ + public function set($code, $value) + { + $this->values[$code] = $value; + + return $this; + } + + /** + * @param $code + * @return bool + */ + public function has($code) + { + return isset($this->values[$code]); + } + + /** + * @param $code + * @return bool + */ + public function is($code) + { + return (bool) $this->get($code); + } + + /** + * @param array $exclude + * @param array $include + * @return array + */ + public function getValues(array $exclude = array(), array $include = array()) + { + switch (true) { + case count($exclude): + return array_diff_key($this->values, array_reverse($exclude)); + break; + case count($include): + return array_intersect_key($this->values, array_reverse($exclude)); + break; + default: + return $this->values; + } + } + + /** + * @param array $values + * @return $this + */ + public function setValues($values) + { + $this->values = $values; + + return $this; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php new file mode 100644 index 00000000000..040b2a98972 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php @@ -0,0 +1,54 @@ +fields = new ArrayCollection(); + $this->className = $className; + $this->scope = $scope; + } + + /** + * @param string $name + * @return $this + */ + public function setClassName($name) + { + $this->className = $name; + + return $this; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * @return string|void + */ + public function getScope() + { + return $this->scope; + } + + /** + * @param FieldConfig[] $fields + * @return $this + */ + public function setFields($fields) + { + foreach ($fields as $field) { + $this->addField($field); + } + + return $this; + } + + /** + * @param FieldConfig $field + */ + public function addField(FieldConfig $field) + { + $this->fields[$field->getCode()] = $field; + } + + /** + * @param $name + * @return FieldConfig + */ + public function getField($name) + { + return $this->fields[$name]; + } + + /** + * @param $name + * @return bool + */ + public function hasField($name) + { + return isset($this->fields[$name]); + } + + /** + * @param callable $filter + * @return FieldConfig[]|ArrayCollection + */ + public function getFields(\Closure $filter = null) + { + return $filter ? $this->fields->filter($filter) : $this->fields; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->className, + $this->scope, + $this->fields, + $this->values, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->className, + $this->scope, + $this->fields, + $this->values, + ) = unserialize($serialized); + } + + /** + * Clone Config + */ + public function __clone() + { + $this->values = array_map(function ($value) { + if (is_object($value)) { + return clone $value; + } else { + return $value; + } + }, $this->values); + + $this->fields = $this->fields->map(function ($field) { + return clone $field; + }, $this->fields); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php new file mode 100644 index 00000000000..11d24164771 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php @@ -0,0 +1,12 @@ +className = $className; + $this->code = $code; + $this->type = $type; + $this->scope = $scope; + } + + /** + * @param string $className + * @return $this + */ + public function setClassName($className) + { + $this->className = $className; + + return $this; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * @param string $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @param string $type + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->code, + $this->className, + $this->type, + $this->scope, + $this->values, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->code, + $this->className, + $this->type, + $this->scope, + $this->values, + ) = unserialize($serialized); + } + + /** + * Clone Config + */ + public function __clone() + { + $this->values = array_map(function ($value) { + if (is_object($value)) { + return clone $value; + } else { + return $value; + } + }, $this->values); + + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php new file mode 100644 index 00000000000..e8e856d8e31 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php @@ -0,0 +1,16 @@ +persistConfigs = new \SplQueue(); + $this->metadataFactory = $metadataFactory; + $this->proxyEm = $proxyEm; + $this->eventDispatcher = $eventDispatcher; + + $this->auditManager = new AuditManager($this, $security); + } + + /** + * @param CacheInterface $cache + */ + public function setCache(CacheInterface $cache) + { + $this->configCache = $cache; + } + + /** + * @return EntityManager + */ + public function em() + { + return $this->proxyEm->getService(); + } + + /** + * @return ConfigProvider[] + */ + public function getProviders() + { + return $this->providers; + } + + /** + * @param ConfigProvider $provider + */ + public function addProvider(ConfigProvider $provider) + { + $this->providers[$provider->getScope()] = $provider; + } + + /** + * @param $scope + * @return ConfigProvider + */ + public function getProvider($scope) + { + return $this->providers[$scope]; + } + + /** + * @return EventDispatcher + */ + public function getEventDispatcher() + { + return $this->eventDispatcher; + } + + /** + * @param $className + * @return \Metadata\ClassMetaData + */ + public function getClassMetadata($className) + { + return $this->metadataFactory->getMetadataForClass($className); + } + + /** + * @param $className + * @param $scope + * @throws Exception\RuntimeException + * @return EntityConfig + */ + public function getConfig($className, $scope) + { + /** @var ConfigClassMetadata $metadata */ + $metadata = $this->metadataFactory->getMetadataForClass($className); + if (!$metadata || $metadata->name != $className || !$metadata->configurable) { + throw new RuntimeException(sprintf("Entity '%s' is not Configurable", $className)); + } + + $resultConfig = null; + if (null !== $this->configCache + && $config = $this->configCache->loadConfigFromCache($className, $scope) + ) { + $resultConfig = $config; + } else { + $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); + + /** @var ConfigEntity $entity */ + $entity = $this->isSchemaSynced() ? $entityConfigRepo->findOneBy(array('className' => $className)) : null; + if ($entity) { + $config = $this->entityToConfig($entity, $scope); + + if (null !== $this->configCache) { + $this->configCache->putConfigInCache($config); + } + + $resultConfig = $config; + } else { + $resultConfig = new EntityConfig($className, $scope); + } + } + + $this->originalConfigs[spl_object_hash($resultConfig)] = clone $resultConfig; + + foreach ($resultConfig->getFields() as $field) { + $this->originalConfigs[spl_object_hash($field)] = clone $field; + } + + return $resultConfig; + } + + /** + * @param $className + * @return bool + */ + public function hasConfig($className) + { + /** @var ConfigClassMetadata $metadata */ + $metadata = $this->metadataFactory->getMetadataForClass($className); + + return $metadata ? ($metadata->configurable && $metadata->name == $className) : false; + } + + /** + * @param ClassMetadataInfo $doctrineMetadata + */ + public function initConfigByDoctrineMetadata(ClassMetadataInfo $doctrineMetadata) + { + /** @var ConfigClassMetadata $metadata */ + $metadata = $this->metadataFactory->getMetadataForClass($doctrineMetadata->getName()); + if ($metadata + && $metadata->name == $doctrineMetadata->getName() + && $metadata->configurable + && !$this->em()->getRepository(ConfigEntity::ENTITY_NAME)->findOneBy(array( + 'className' => $doctrineMetadata->getName())) + ) { + foreach ($this->getProviders() as $provider) { + $defaultValues = $provider->getConfigContainer()->getEntityDefaultValues(); + if (isset($metadata->defaultValues[$provider->getScope()])) { + $defaultValues = array_merge($defaultValues, $metadata->defaultValues[$provider->getScope()]); + } + + $provider->createEntityConfig($doctrineMetadata->getName(), $defaultValues); + } + + $this->eventDispatcher->dispatch( + Events::NEW_ENTITY, + new NewEntityEvent($doctrineMetadata->getName(), $this) + ); + + foreach ($doctrineMetadata->getFieldNames() as $fieldName) { + $type = $doctrineMetadata->getTypeOfField($fieldName); + + foreach ($this->getProviders() as $provider) { + $provider->createFieldConfig( + $doctrineMetadata->getName(), + $fieldName, + $type, + $provider->getConfigContainer()->getFieldDefaultValues() + ); + } + + $this->eventDispatcher->dispatch( + Events::NEW_FIELD, + new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) + ); + } + + foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { + $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; + + foreach ($this->getProviders() as $provider) { + $provider->createFieldConfig( + $doctrineMetadata->getName(), + $fieldName, + $type, + $provider->getConfigContainer()->getFieldDefaultValues() + ); + } + + $this->eventDispatcher->dispatch( + Events::NEW_FIELD, + new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) + ); + } + } + } + + /** + * @param $className + */ + public function clearCache($className) + { + if ($this->configCache) { + foreach ($this->getProviders() as $provider) { + $this->configCache->removeConfigFromCache($className, $provider->getScope()); + } + } + } + + /** + * @param ConfigInterface $config + */ + public function persist(ConfigInterface $config) + { + $this->persistConfigs->push($config); + + if ($config instanceof EntityConfigInterface) { + foreach ($config->getFields() as $fieldConfig) { + $this->persistConfigs->push($fieldConfig); + } + } + } + + /** + * @param ConfigInterface $config + */ + public function remove(ConfigInterface $config) + { + $this->removeConfigs[spl_object_hash($config)] = $config; + + if ($config instanceof EntityConfigInterface) { + foreach ($config->getFields() as $fieldConfig) { + $this->removeConfigs[spl_object_hash($fieldConfig)] = $fieldConfig; + } + } + } + + /** + * TODO:: remove configs + */ + public function flush() + { + $entities = array(); + + foreach ($this->persistConfigs as $config) { + $className = $config->getClassName(); + + if (isset($entities[$className])) { + $configEntity = $entities[$className]; + } else { + $configEntity = $entities[$className] = $this->findOrCreateConfigEntity($className); + } + + $this->eventDispatcher->dispatch(Events::PERSIST_CONFIG, new PersistConfigEvent($config, $this)); + + $this->calculateConfigChangeSet($config); + + $changes = $this->getConfigChangeSet($config); + + if (!count($changes)) { + continue; + } + + $values = array_intersect_key($config->getValues(), $changes); + + if ($config instanceof FieldConfigInterface) { + if (!$configField = $configEntity->getField($config->getCode())) { + $configField = new ConfigField($config->getCode(), $config->getType()); + $configEntity->addField($configField); + } + + $serializableValues = $this->getProvider($config->getScope())->getConfigContainer()->getFieldSerializableValues(); + $configField->fromArray($config->getScope(), $values, $serializableValues); + } else { + $serializableValues = $this->getProvider($config->getScope())->getConfigContainer()->getEntitySerializableValues(); + $configEntity->fromArray($config->getScope(), $values, $serializableValues); + } + + + if ($this->configCache) { + $this->configCache->removeConfigFromCache($className, $config->getScope()); + } + } + + $this->eventDispatcher->dispatch(Events::PRE_FLUSH, new FlushConfigEvent($this)); + + $this->auditManager->log(); + + foreach ($entities as $entity) { + $this->em()->persist($entity); + } + + $this->em()->flush(); + + $this->eventDispatcher->dispatch(Events::ON_FLUSH, new FlushConfigEvent($this)); + + $this->removeConfigs = array(); + $this->originalConfigs = array(); + $this->configChangeSets = array(); + $this->updatedConfigs = array(); + + + $this->eventDispatcher->dispatch(Events::POST_FLUSH, new FlushConfigEvent($this)); + } + + + /** + * @param ConfigInterface $config + */ + public function calculateConfigChangeSet(ConfigInterface $config) + { + $originConfigValue = array(); + if (isset($this->originalConfigs[spl_object_hash($config)])) { + $originConfig = $this->originalConfigs[spl_object_hash($config)]; + $originConfigValue = $originConfig->getValues(); + } + + foreach ($config->getValues() as $key => $value) { + if (!isset($originConfigValue[$key])) { + $originConfigValue[$key] = null; + } + } + + $diffNew = array_udiff_assoc($config->getValues(), $originConfigValue, function ($a, $b) { + return ($a == $b) ? 0 : 1; + }); + + $diffOld = array_udiff_assoc($originConfigValue, $config->getValues(), function ($a, $b) { + return ($a == $b) ? 0 : 1; + }); + + $diff = array(); + foreach ($diffNew as $key => $value) { + $oldValue = isset($diffOld[$key]) ? $diffOld[$key] : null; + $diff[$key] = array($oldValue, $value); + } + + + if (!isset($this->configChangeSets[spl_object_hash($config)])) { + $this->configChangeSets[spl_object_hash($config)] = array(); + } + + if (count($diff)) { + $this->configChangeSets[spl_object_hash($config)] = array_merge($this->configChangeSets[spl_object_hash($config)], $diff); + + if (!isset($this->updatedConfigs[spl_object_hash($config)])) { + $this->updatedConfigs[spl_object_hash($config)] = $config; + } + } + } + + /** + * @param null $scope + * @return ConfigInterface[]|EntityConfigInterface[] + */ + public function getUpdatedEntityConfig($scope = null) + { + return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($scope) { + if (!$config instanceof EntityConfigInterface) { + return false; + } + + if ($scope && $config->getScope() != $scope) { + return false; + } + + return true; + }); + } + + /** + * @param null $className + * @param null $scope + * @return ConfigInterface[]|FieldConfigInterface[] + */ + public function getUpdatedFieldConfig($scope = null, $className = null) + { + return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($className, $scope) { + if (!$config instanceof FieldConfigInterface) { + return false; + } + + if ($className && $config->getClassName() != $className) { + return false; + } + + if ($scope && $config->getScope() != $scope) { + return false; + } + + return true; + }); + } + + /** + * @param ConfigInterface $config + * @return array + */ + public function getConfigChangeSet(ConfigInterface $config) + { + return isset($this->configChangeSets[spl_object_hash($config)]) ? $this->configChangeSets[spl_object_hash($config)] : array(); + } + + /** + * @param ConfigEntity $entity + * @param $scope + * @return EntityConfig + */ + protected function entityToConfig(ConfigEntity $entity, $scope) + { + $config = new EntityConfig($entity->getClassName(), $scope); + $config->setValues($entity->toArray($scope)); + + foreach ($entity->getFields() as $field) { + $fieldConfig = new FieldConfig($entity->getClassName(), $field->getCode(), $field->getType(), $scope); + $fieldConfig->setValues($field->toArray($scope)); + $config->addField($fieldConfig); + } + + return $config; + } + + /** + * @param $className + * @return ConfigEntity + */ + protected function findOrCreateConfigEntity($className) + { + $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); + /** @var ConfigEntity $entity */ + $entity = $entityConfigRepo->findOneBy(array('className' => $className)); + if (!$entity) { + $metadata = $this->metadataFactory->getMetadataForClass($className); + $entity = new ConfigEntity($className); + $entity->setMode($metadata->viewMode); + + foreach ($this->getProviders() as $provider) { + $provider->createEntityConfig( + $className, + $provider->getConfigContainer()->getEntityDefaultValues() + ); + } + + $this->eventDispatcher->dispatch( + Events::NEW_ENTITY, + new NewEntityEvent($className, $this) + ); + } + + return $entity; + } + + /** + * @return bool + */ + protected function isSchemaSynced() + { + $tables = $this->em()->getConnection()->getSchemaManager()->listTableNames(); + $table = $this->em()->getClassMetadata(ConfigEntity::ENTITY_NAME)->getTableName(); + + return in_array($table, $tables); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php new file mode 100644 index 00000000000..3114ce8c418 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php @@ -0,0 +1,110 @@ +get('oro_entity_config.audit_datagrid.manager'); + + $datagridManager->entityClass = str_replace('_', '\\', $entity); + $datagridManager->entityClassId = $id; + + $datagridManager->getRouteGenerator()->setRouteParameters( + array( + 'entity' => $entity, + 'id' => $id + ) + ); + + $view = $datagridManager->getDatagrid()->createView(); + + return 'json' == $this->getRequest()->getRequestFormat() + ? $this->get('oro_grid.renderer')->renderResultsJsonResponse($view) + : $this->render('OroEntityConfigBundle:Audit:audit.html.twig', array('datagrid' => $view)); + } + + /** + * @Route( + * "/audit_field/{entity}/{id}/{_format}", + * name="oro_entityconfig_audit_field", + * requirements={"entity"="[a-zA-Z_]+", "id"="\d+"}, + * defaults={"entity"="entity", "id"=0, "_format" = "html"} + * ) + * @Acl( + * id="oro_entityconfig_audit_field", + * name="View entity's field history", + * description="View entity's field history audit log", + * parent="oro_entityconfig" + * ) + * + * @param $entity + * @param $id + * @return \Symfony\Component\HttpFoundation\Response + */ + public function auditFieldAction($entity, $id) + { + /** @var ConfigField $fieldName */ + $fieldName = $this->getDoctrine() + ->getRepository('OroEntityConfigBundle:ConfigField') + ->findOneBy(array('id' => $id)); + + /** @var $datagridManager AuditFieldDatagridManager */ + $datagridManager = $this->get('oro_entity_config.audit_field_datagrid.manager'); + + $datagridManager->entityClass = str_replace('_', '\\', $entity); + $datagridManager->fieldName = $fieldName->getCode(); + + $datagridManager->getRouteGenerator()->setRouteParameters( + array( + 'entity' => $entity, + 'id' => $id + ) + ); + + $view = $datagridManager->getDatagrid()->createView(); + + return 'json' == $this->getRequest()->getRequestFormat() + ? $this->get('oro_grid.renderer')->renderResultsJsonResponse($view) + : $this->render('OroEntityConfigBundle:Audit:audit.html.twig', array('datagrid' => $view)); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php new file mode 100644 index 00000000000..f45d1e3afb2 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php @@ -0,0 +1,288 @@ +get('oro_entity_config.datagrid.manager'); + $datagrid = $datagridManager->getDatagrid(); + $view = 'json' == $request->getRequestFormat() + ? 'OroGridBundle:Datagrid:list.json.php' + : 'OroEntityConfigBundle:Config:index.html.twig'; + + return $this->render( + $view, + array( + 'buttonConfig' => $datagridManager->getLayoutActions(), + 'datagrid' => $datagrid->createView() + ) + ); + } + + /** + * @Route("/update/{id}", name="oro_entityconfig_update") + * @Acl( + * id="oro_entityconfig_update", + * name="Update entity", + * description="Update configurable entity", + * parent="oro_entityconfig" + * ) + * @Template() + */ + public function updateAction($id) + { + $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $request = $this->getRequest(); + + $form = $this->createForm( + 'oro_entity_config_config_entity_type', + null, + array( + 'class_name' => $entity->getClassName(), + 'entity_id' => $entity->getId() + ) + ); + + if ($request->getMethod() == 'POST') { + $form->bind($request); + + if ($form->isValid()) { + //persist data inside the form + $this->get('session')->getFlashBag()->add('success', 'ConfigEntity successfully saved'); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_entityconfig_update', + 'parameters' => array('id' => $id), + ), + array( + 'route' => 'oro_entityconfig_index' + ) + ); + } + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + return array( + 'entity' => $entity, + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + 'form' => $form->createView(), + ); + } + + /** + * View Entity + * @Route("/view/{id}", name="oro_entityconfig_view") + * @Acl( + * id="oro_entityconfig_view", + * name="View entity", + * description="View configurable entity", + * parent="oro_entityconfig" + * ) + * @Template() + */ + public function viewAction(ConfigEntity $entity) + { + /** @var EntityFieldsDatagridManager $datagridManager */ + $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); + $datagridManager->setEntityId($entity->getId()); + $datagridManager->getRouteGenerator()->setRouteParameters( + array( + 'id' => $entity->getId() + ) + ); + + $datagrid = $datagridManager->getDatagrid(); + + /** + * define Entity module and name + */ + $entityName = $moduleName = ''; + $className = explode('\\', $entity->getClassName()); + foreach ($className as $i => $name) { + if (count($className) - 1 == $i) { + $entityName = $name; + } elseif (!in_array($name, array('Bundle', 'Entity'))) { + $moduleName .= $name; + } + } + + /** + * generate link for Entity grid + */ + + /** @var \Oro\Bundle\EntityConfigBundle\ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + + /** @var ConfigClassMetadata $metadata */ + $metadata = $configManager->getClassMetadata($entity->getClassName()); + + $link = ''; + if ($metadata->routeName) { + $link = $this->generateUrl($metadata->routeName); + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + /** @var ConfigProvider $extendConfigProvider */ + $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + + /* + var_dump($this->getRequest()->headers->get('referer')); + if (strstr('oro_entityextend/update', $this->getRequest()->headers->get('referer'))) { + $this->get('session')->getFlashBag()->add('success', 'Schema successfully updated.'); + }*/ + + return array( + 'entity' => $entity, + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + 'entity_extend' => $extendConfig, + 'entity_count' => count($this->getDoctrine()->getRepository($entity->getClassName())->findAll()), + 'entity_fields' => $datagrid->createView(), + + 'unique_key' => $extendConfig->get('unique_key'), + 'link' => $link, + 'entity_name' => $entityName, + 'module_name' => $moduleName, + 'button_config' => $datagridManager->getLayoutActions($entity), + ); + } + + /** + * Lists Entity fields + * @Route("/fields/{id}", name="oro_entityconfig_fields", requirements={"id"="\d+"}, defaults={"id"=0}) + * @Template() + */ + public function fieldsAction($id, Request $request) + { + $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + + /** @var FieldsDatagridManager $datagridManager */ + $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); + $datagridManager->setEntityId($id); + + $datagrid = $datagridManager->getDatagrid(); + + $datagridManager->getRouteGenerator()->setRouteParameters( + array( + 'id' => $id + ) + ); + + $view = 'json' == $request->getRequestFormat() + ? 'OroGridBundle:Datagrid:list.json.php' + : 'OroEntityConfigBundle:Config:fields.html.twig'; + + return $this->render( + $view, + array( + 'buttonConfig' => $datagridManager->getLayoutActions($entity), + 'datagrid' => $datagrid->createView(), + 'entity_id' => $id, + 'entity_name' => $entity->getClassName(), + ) + ); + } + + /** + * @Route("/field/update/{id}", name="oro_entityconfig_field_update") + * @Acl( + * id="oro_entityconfig_field_update", + * name="Update entity field", + * description="Update configurable entity field", + * parent="oro_entityconfig" + * ) + * @Template() + */ + public function fieldUpdateAction($id) + { + $field = $this->getDoctrine()->getRepository(ConfigField::ENTITY_NAME)->find($id); + + $form = $this->createForm( + 'oro_entity_config_config_field_type', + null, + array( + 'class_name' => $field->getEntity()->getClassName(), + 'field_name' => $field->getCode(), + 'field_type' => $field->getType(), + 'field_id' => $field->getId(), + ) + ); + $request = $this->getRequest(); + + if ($request->getMethod() == 'POST') { + $form->bind($request); + + if ($form->isValid()) { + //persist data inside the form + $this->get('session')->getFlashBag()->add('success', 'ConfigField successfully saved'); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_entityconfig_field_update', + 'parameters' => array('id' => $id), + ), + array( + 'route' => 'oro_entityconfig_view', + 'parameters' => array('id' => $field->getEntity()->getId()) + ) + ); + } + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + return array( + 'entity_config' => $entityConfigProvider->getConfig($field->getEntity()->getClassName()), + 'field_config' => $entityConfigProvider->getFieldConfig($field->getEntity()->getClassName(), $field->getCode()), + 'field' => $field, + 'form' => $form->createView(), + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php new file mode 100644 index 00000000000..f382890594e --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php @@ -0,0 +1,150 @@ +configManager = $configManager; + } + + /** + * @var string + */ + protected $authorExpression = + 'CONCAT( + CONCAT( + CONCAT(user.firstName, \' \'), + CONCAT(user.lastName, \' \') + ), + CONCAT(\' - \', user.email) + )'; + + /** + * {@inheritDoc} + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $fieldId = new FieldDescription(); + $fieldId->setName('id'); + $fieldId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => 'Commit Id', + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'required' => false, + 'sortable' => true, + 'filterable' => false, + 'show_filter' => false, + 'show_column' => false, + ) + ); + $fieldsCollection->add($fieldId); + + $fieldAuthor = new FieldDescription(); + $fieldAuthor->setName('author'); + $fieldAuthor->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Author', + 'field_name' => 'author', + 'expression' => $this->authorExpression, + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => false, + 'show_filter' => false, + ) + ); + $fieldsCollection->add($fieldAuthor); + + $logDiffs = new FieldDescription(); + $logDiffs->setName('diffs'); + $logDiffs->setProperty(new FixedProperty('diffs', 'diffs')); + $logDiffs->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_HTML, + 'label' => 'Diffs', + 'field_name' => 'diffs', + 'expression' => 'diff', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => false, + 'filterable' => false, + 'show_filter' => false, + ) + ); + $templateDiffProperty = new TwigTemplateProperty( + $logDiffs, + 'OroEntityConfigBundle:Audit:data.html.twig', + array_merge( + $this->getOptions(), + array('config_manager' => $this->configManager) + ) + ); + $logDiffs->setProperty($templateDiffProperty); + $fieldsCollection->add($logDiffs); + + $fieldCreated = new FieldDescription(); + $fieldCreated->setName('loggedAt'); + $fieldCreated->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_DATETIME, + 'label' => 'Logged at', + 'field_name' => 'loggedAt', + 'filter_type' => FilterInterface::TYPE_DATETIME, + 'required' => false, + 'sortable' => true, + 'filterable' => false, + 'show_filter' => false, + ) + ); + $fieldsCollection->add($fieldCreated); + } + + abstract protected function getOptions(); + + /** + * {@inheritDoc} + */ + protected function getDefaultSorters() + { + return array( + 'loggedAt' => SorterInterface::DIRECTION_DESC + ); + } + + /** + * @param ProxyQueryInterface $query + * @return ProxyQueryInterface|void + */ + protected function prepareQuery(ProxyQueryInterface $query) + { + $query->addSelect($this->authorExpression . ' AS author', true); + $query->leftJoin('log.user', 'user'); + + return $query; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagridManager.php new file mode 100644 index 00000000000..4e5d085e7fe --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagridManager.php @@ -0,0 +1,29 @@ +innerJoin('log.diffs', 'diff', 'WITH', 'diff.className = :className AND diff.fieldName IS NULL'); + $query->setParameter('className', $this->entityClass); + + return $query; + } + + protected function getOptions() + { + return array( + 'is_entity' => true + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditFieldDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditFieldDatagridManager.php new file mode 100644 index 00000000000..af6ba72acda --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditFieldDatagridManager.php @@ -0,0 +1,40 @@ +innerJoin('log.diffs', 'diff', 'WITH', 'diff.className = :className AND diff.fieldName = :fieldName'); + $query->setParameters( + array( + 'className' => $this->entityClass, + 'fieldName' => $this->fieldName, + ) + ); + + return $query; + } + + protected function getOptions() + { + return array( + 'is_entity' => false, + 'field_name' => $this->fieldName + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php new file mode 100644 index 00000000000..ae3a27c22f5 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php @@ -0,0 +1,328 @@ +configManager = $configManager; + } + + /** + * @return array + */ + public function getLayoutActions() + { + $actions = array(); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getEntityLayoutActions() as $config) { + $actions[] = $config; + } + } + + return $actions; + } + + /** + * {@inheritDoc} + */ + protected function getProperties() + { + $properties = array( + new UrlProperty('view_link', $this->router, 'oro_entityconfig_view', array('id')), + new UrlProperty('update_link', $this->router, 'oro_entityconfig_update', array('id')), + ); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getEntityGridActions() as $config) { + $properties[] = new UrlProperty( + strtolower($config['name']) . '_link', + $this->router, + $config['route'], + (isset($config['args']) ? $config['args'] : array()) + ); + } + } + + return $properties; + } + + /** + * @param string $scope + * @return array + */ + protected function getObjectName($scope = 'name') + { + $options = array('name'=> array(), 'module'=> array()); + + $query = $this->createQuery()->getQueryBuilder() + ->add('select', 'ce.className') + ->distinct('ce.className'); + + $result = $query->getQuery()->getArrayResult(); + + foreach ((array) $result as $value) { + $className = explode('\\', $value['className']); + + $options['name'][$value['className']] = ''; + $options['module'][$value['className']] = ''; + + foreach ($className as $index => $name) { + if (count($className)-1 == $index) { + $options['name'][$value['className']] = $name; + } elseif (!in_array($name, array('Bundle','Entity'))) { + $options['module'][$value['className']] .= $name; + } + } + } + + return $options[$scope]; + } + + /** + * @param FieldDescriptionCollection $fieldsCollection + */ + protected function getDynamicFields(FieldDescriptionCollection $fieldsCollection) + { + $fields = array(); + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getEntityItems() as $code => $item) { + if (isset($item['grid'])) { + $fieldObjectProvider = new FieldDescription(); + $fieldObjectProvider->setName($code); + $fieldObjectProvider->setOptions( + array_merge( + $item['grid'], + array( + 'expression' => 'cev' . $code . '.value', + 'field_name' => $code, + ) + ) + ); + + if (isset($item['options']['priority']) && !isset($fields[$item['options']['priority']])) { + $fields[$item['options']['priority']] = $fieldObjectProvider; + } else { + $fields[] = $fieldObjectProvider; + } + } + } + } + + ksort($fields); + foreach ($fields as $field) { + $fieldsCollection->add($field); + } + } + + /** + * {@inheritDoc} + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $this->getDynamicFields($fieldsCollection); + + $fieldObjectId = new FieldDescription(); + $fieldObjectId->setName('id'); + $fieldObjectId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => 'Id', + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'required' => false, + 'sortable' => false, + 'filterable' => false, + 'show_filter' => false, + 'show_column' => false, + ) + ); + $fieldsCollection->add($fieldObjectId); + + $fieldObjectName = new FieldDescription(); + $fieldObjectName->setName('name'); + $fieldObjectName->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_OPTIONS, + 'label' => 'Name', + 'field_name' => 'className', + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + 'choices' => $this->getObjectName(), + 'multiple' => true, + ) + ); + $fieldsCollection->add($fieldObjectName); + + $fieldObjectModule = new FieldDescription(); + $fieldObjectModule->setName('module'); + $fieldObjectModule->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_OPTIONS, + 'label' => 'Module', + 'field_name' => 'className', + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + 'choices' => $this->getObjectName('module'), + 'multiple' => true, + ) + ); + $fieldsCollection->add($fieldObjectModule); + + $fieldObjectCreate = new FieldDescription(); + $fieldObjectCreate->setName('created'); + $fieldObjectCreate->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_DATETIME, + 'label' => 'Create At', + 'field_name' => 'created', + 'filter_type' => FilterInterface::TYPE_DATETIME, + 'required' => true, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldObjectCreate); + + $fieldObjectUpdate = new FieldDescription(); + $fieldObjectUpdate->setName('updated'); + $fieldObjectUpdate->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_DATETIME, + 'label' => 'Update At', + 'field_name' => 'updated', + 'filter_type' => FilterInterface::TYPE_DATETIME, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldObjectUpdate); + } + + /** + * {@inheritDoc} + */ + protected function getRowActions() + { + $clickAction = array( + 'name' => 'rowClick', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'root', + 'options' => array( + 'label' => 'View', + 'link' => 'view_link', + 'runOnRowClick' => true, + ) + ); + + $viewAction = array( + 'name' => 'view', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'root', + 'options' => array( + 'label' => 'View', + 'icon' => 'book', + 'link' => 'view_link', + ) + ); + + $updateAction = array( + 'name' => 'update', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'root', + 'options' => array( + 'label' => 'Edit', + 'icon' => 'edit', + 'link' => 'update_link', + ) + ); + + $actions = array($clickAction, $viewAction, $updateAction); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getEntityGridActions() as $config) { + $configItem = array( + 'name' => strtolower($config['name']), + 'acl_resource' => isset($config['acl_resource']) ? $config['acl_resource'] : 'root', + 'options' => array( + 'label' => ucfirst($config['name']), + 'icon' => isset($config['icon']) ? $config['icon'] : 'question-sign', + 'link' => strtolower($config['name']) . '_link' + ) + ); + + if (isset($config['type'])) { + switch ($config['type']) { + case 'delete': + $configItem['type'] = ActionInterface::TYPE_DELETE; + break; + case 'redirect': + $configItem['type'] = ActionInterface::TYPE_REDIRECT; + break; + } + } else { + $configItem['type'] = ActionInterface::TYPE_REDIRECT; + } + + $actions[] = $configItem; + } + } + + return $actions; + } + + /** + * @param ProxyQueryInterface $query + * @return ProxyQueryInterface + */ + protected function prepareQuery(ProxyQueryInterface $query) + { + $query->where('ce.mode <> :mode'); + $query->setParameter('mode', AbstractConfig::MODE_VIEW_HIDDEN); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getEntityItems() as $code => $item) { + $alias = 'cev' . $code; + $query->leftJoin('ce.values', $alias, 'WITH', $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'"); + $query->addSelect($alias . '.value as ' . $code, true); + } + } + + return $query; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php new file mode 100644 index 00000000000..f13954b6394 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php @@ -0,0 +1,267 @@ +configManager = $configManager; + } + + /** + * @param $id + */ + public function setEntityId($id) + { + $this->entityId = $id; + } + + /** + * @param ConfigEntity $entity + * @return array + */ + public function getLayoutActions(ConfigEntity $entity) + { + $actions = array(); + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getFieldLayoutActions() as $config) { + if (isset($config['filter']) + && !$provider->getConfig($entity->getClassName())->is($config['filter']) + ) { + continue; + } + + if (isset($config['entity_id']) && $config['entity_id'] == true) { + $config['args'] = array('id' => $entity->getId()); + } + $actions[] = $config; + } + } + + return $actions; + } + + /** + * {@inheritDoc} + */ + protected function getProperties() + { + $properties = array( + new UrlProperty('update_link', $this->router, 'oro_entityconfig_field_update', array('id')), + ); + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getFieldGridActions() as $config) { + $properties[] = new UrlProperty( + strtolower($config['name']) . '_link', + $this->router, + $config['route'], + (isset($config['args']) ? $config['args'] : array()) + ); + } + } + + return $properties; + } + + /** + * {@inheritDoc} + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $fieldId = new FieldDescription(); + $fieldId->setName('id'); + $fieldId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Id', + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => false, + 'filterable' => false, + 'show_filter' => false, + 'show_column' => false, + ) + ); + $fieldsCollection->add($fieldId); + + $fieldCode = new FieldDescription(); + $fieldCode->setName('code'); + $fieldCode->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Name', + 'field_name' => 'code', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => false, + 'show_filter' => false, + ) + ); + $fieldsCollection->add($fieldCode); + + $fieldType = new FieldDescription(); + $fieldType->setName('type'); + $fieldType->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Data Type', + 'field_name' => 'type', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => false, + 'show_filter' => false, + ) + ); + $fieldsCollection->add($fieldType); + + $this->addDynamicRows($fieldsCollection); + } + + /** + * @param $fieldsCollection + */ + protected function addDynamicRows($fieldsCollection) + { + $fields = array(); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getFieldItems() as $code => $item) { + if (isset($item['grid'])) { + $fieldObject = new FieldDescription(); + $fieldObject->setName($code); + $fieldObject->setOptions(array_merge($item['grid'], array( + 'expression' => 'cfv_' . $code . '.value', + 'field_name' => $code, + ))); + + if (isset($item['options']['priority']) && !isset($fields[$item['options']['priority']])) { + $fields[$item['options']['priority']] = $fieldObject; + } else { + $fields[] = $fieldObject; + } + } + } + } + + ksort($fields); + foreach ($fields as $field) { + $fieldsCollection->add($field); + } + } + + /** + * {@inheritDoc} + */ + protected function getRowActions() + { + $clickAction = array( + 'name' => 'rowClick', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'root', + 'options' => array( + 'label' => 'Edit', + 'link' => 'update_link', + 'runOnRowClick' => true, + ) + ); + + $updateAction = array( + 'name' => 'update', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'root', + 'options' => array( + 'label' => 'Edit', + 'icon' => 'edit', + 'link' => 'update_link', + ) + ); + + $actions = array($clickAction, $updateAction); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getFieldGridActions() as $config) { + $configItem = array( + 'name' => strtolower($config['name']), + 'acl_resource' => isset($config['acl_resource']) ? $config['acl_resource'] : 'root', + 'options' => array( + 'label' => ucfirst($config['name']), + 'icon' => isset($config['icon']) ? $config['icon'] : 'question-sign', + 'link' => strtolower($config['name']) . '_link' + ) + ); + + if (isset($config['type'])) { + switch ($config['type']) { + case 'delete': + $configItem['type'] = ActionInterface::TYPE_DELETE; + break; + case 'redirect': + $configItem['type'] = ActionInterface::TYPE_REDIRECT; + break; + } + } else { + $configItem['type'] = ActionInterface::TYPE_REDIRECT; + } + + $actions[] = $configItem; + } + } + + return $actions; + } + + /** + * @return ProxyQueryInterface + */ + protected function createQuery() + { + /** @var ProxyQueryInterface|Query $query */ + $query = parent::createQuery(); + $query->innerJoin('cf.entity', 'ce', 'WITH', 'ce.id=' . $this->entityId); + $query->addSelect('ce.id as entity_id', true); + + foreach ($this->configManager->getProviders() as $provider) { + foreach ($provider->getConfigContainer()->getFieldItems() as $code => $item) { + //$code = $provider->getScope() . $code; + $alias = 'cfv_' . $code; + $query->leftJoin('cf.values', $alias, 'WITH', $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'"); + $query->addSelect($alias . '.value as ' . $code, true); + } + } + + return $query; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php new file mode 100644 index 00000000000..d2f314fbde9 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php @@ -0,0 +1,50 @@ +getDefinition('oro_entity_config.config_manager'); + + $tags = $container->findTaggedServiceIds(self::TAG_NAME); + foreach ($tags as $id => $tag) { + /** @var Definition $provider */ + $provider = $container->getDefinition($id); + + if (!isset($tag[0]['scope'])) { + throw new RuntimeException( + sprintf("Tag '%s' for service '%s' doesn't have required param 'scope'", self::TAG_NAME, $id) + ); + } + + if (!$container->hasDefinition('oro_entity_config.entity_config.' . $tag[0]['scope'])) { + throw new RuntimeException(sprintf( + "Resources/config/entity_config.yml not found or has wrong 'scope'. Service '%s' with tag '%s' and tag-scope '%s' ", + $id, self::TAG_NAME, $tag[0]['scope'] + )); + } + + $provider->setClass('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider'); + $provider->setArguments(array( + new Reference('oro_entity_config.config_manager'), + new Reference('oro_entity_config.entity_config.' . $tag[0]['scope']) + )); + + $configManagerDefinition->addMethodCall('addProvider', array($provider)); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php new file mode 100644 index 00000000000..56971dcb257 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php @@ -0,0 +1,47 @@ +findTaggedServiceIds(self::TAG_NAME); + + foreach ($tags as $id => $tag) { + /** @var Definition $service */ + $service = $container->getDefinition($id); + + if (!isset($tag[0]['service'])) { + throw new RuntimeException( + sprintf("Tag '%s' for service '%s' doesn't have required param 'service'", self::TAG_NAME, $id) + ); + } + + if (!$container->hasDefinition($tag[0]['service'])) { + throw new RuntimeException(sprintf( + "Target service '%s' is undefined. Proxy Service '%s' with tag '%s' and tag-service '%s' ", + $tag[0]['service'], $id, self::TAG_NAME, $tag[0]['service'] + )); + } + + $service->setClass('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy'); + $service->setArguments(array( + new Reference('service_container'), + $tag[0]['service'] + )); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..6df7a3cf135 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Configuration.php @@ -0,0 +1,23 @@ +root('oro_entity_config') + ->children() + ->scalarNode('cache_dir')->cannotBeEmpty()->defaultValue('%kernel.cache_dir%/oro_entity_config')->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php new file mode 100644 index 00000000000..cc2ea77f68c --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php @@ -0,0 +1,296 @@ +config = $config; + $this->scope = $scope; + } + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return array + */ + public function getEntityItems() + { + $entityItems = array(); + if (isset($this->config['entity']) && isset($this->config['entity']['items'])) { + $entityItems = $this->config['entity']['items']; + } + + return $entityItems; + } + + /** + * @return array + */ + public function getEntityDefaultValues() + { + $result = array(); + foreach ($this->getEntityItems() as $code => $item) { + if (isset($item['options']['default_value'])) { + $result[$code] = $item['options']['default_value']; + } + } + + return $result; + } + + /** + * @return array + */ + public function getEntityInternalValues() + { + $result = array(); + foreach ($this->getEntityItems() as $code => $item) { + if (isset($item['options']['internal']) && $item['options']['internal']) { + $result[$code] = 0; + } + } + + return $result; + } + + /** + * @return array + */ + public function getEntitySerializableValues() + { + $result = array(); + foreach ($this->getEntityItems() as $code => $item) { + if (isset($item['options']['serializable'])) { + $result[$code] = (bool) $item['options']['serializable']; + } + } + + return $result; + } + + /** + * @return bool + */ + public function hasEntityForm() + { + return (boolean) array_filter($this->getEntityItems(), function ($item) { + return (isset($item['form']) && isset($item['form']['type'])); + }); + } + + /** + * @return array + */ + public function getEntityFormBlockConfig() + { + $entityFormBlockConfig = null; + if (isset($this->config['entity']) + && isset($this->config['entity']['form']) + && isset($this->config['entity']['form']['block_config']) + ) { + $entityFormBlockConfig = $this->config['entity']['form']['block_config']; + } + + return $entityFormBlockConfig; + } + + /** + * @return array + */ + public function getEntityGridActions() + { + $entityGridActions = array(); + if (isset($this->config['entity']) && isset($this->config['entity']['grid_action'])) { + $entityGridActions = $this->config['entity']['grid_action']; + } + + return $entityGridActions; + } + + /** + * @return array + */ + public function getEntityLayoutActions() + { + $entityLayoutActions = array(); + if (isset($this->config['entity']) && isset($this->config['entity']['layout_action'])) { + $entityLayoutActions = $this->config['entity']['layout_action']; + } + + return $entityLayoutActions; + } + + /** + * @return array + */ + public function getFieldItems() + { + $fieldItems = array(); + if (isset($this->config['field']) && isset($this->config['field']['items'])) { + $fieldItems = $this->config['field']['items']; + } + + return $fieldItems; + } + + /** + * @return array + */ + public function getFieldDefaultValues() + { + $result = array(); + foreach ($this->getFieldItems() as $code => $item) { + if (isset($item['options']['default_value'])) { + $result[$code] = $item['options']['default_value']; + } + } + + return $result; + } + + /** + * @return array + */ + public function getFieldInternalValues() + { + $result = array(); + foreach ($this->getFieldItems() as $code => $item) { + if (isset($item['options']['internal']) && $item['options']['internal']) { + $result[$code] = true; + } + } + + return $result; + } + + + /** + * @return array + */ + public function getFieldRequiredPropertyValues() + { + $result = array(); + foreach ($this->getFieldItems() as $code => $item) { + if (isset($item['options']['required_property'])) { + $result[$code] = $item['options']['required_property']; + } + } + + return $result; + } + + /** + * @return array + */ + public function getFieldSerializableValues() + { + $result = array(); + foreach ($this->getFieldItems() as $code => $item) { + if (isset($item['options']['serializable'])) { + $result[$code] = (bool) $item['options']['serializable']; + } + } + + return $result; + } + + /** + * @return bool + */ + public function hasFieldForm() + { + return (boolean) array_filter($this->getFieldItems(), function ($item) { + return (isset($item['form']) && isset($item['form']['type'])); + }); + } + + /** + * @return array + */ + public function getFieldFormConfig() + { + $fieldFormConfig = array(); + if (isset($this->config['field']) && isset($this->config['field']['form'])) { + $fieldFormConfig = $this->config['field']['form']; + } + + return $fieldFormConfig; + } + + /** + * @return array + */ + public function getFieldFormBlockConfig() + { + $entityFormBlockConfig = null; + if (isset($this->config['field']) + && isset($this->config['field']['form']) + && isset($this->config['field']['form']['block_config']) + ) { + $entityFormBlockConfig = $this->config['field']['form']['block_config']; + } + + return $entityFormBlockConfig; + } + + /** + * @return array + */ + public function getFieldGridActions() + { + $fieldGridActions = array(); + if (isset($this->config['field']) && isset($this->config['field']['grid_action'])) { + $fieldGridActions = $this->config['field']['grid_action']; + } + + return $fieldGridActions; + } + + /** + * @return array + */ + public function getFieldLayoutActions() + { + $fieldLayoutActions = array(); + if (isset($this->config['field']) && isset($this->config['field']['layout_action'])) { + $fieldLayoutActions = $this->config['field']['layout_action']; + } + + return $fieldLayoutActions; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php new file mode 100644 index 00000000000..7296d42de46 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php @@ -0,0 +1,92 @@ +loadBundleConfig($container); + + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $this->configCache($container, $config); + + $fileLocator = new FileLocator(__DIR__ . '/../Resources/config'); + $loader = new Loader\YamlFileLoader($container, $fileLocator); + $loader->load('services.yml'); + $loader->load('metadata.yml'); + $loader->load('datagrid.yml'); + $loader->load('form_type.yml'); + } + + protected function loadBundleConfig(ContainerBuilder $container) + { + foreach ($container->getParameter('kernel.bundles') as $bundle) { + $reflection = new \ReflectionClass($bundle); + if (is_file($file = dirname($reflection->getFilename()) . '/Resources/config/entity_config.yml')) { + $bundleConfig = Yaml::parse(realpath($file)); + + if (isset($bundleConfig['oro_entity_config']) && count($bundleConfig['oro_entity_config'])) { + foreach ($bundleConfig['oro_entity_config'] as $scope => $config) { + $definition = new Definition( + 'Oro\Bundle\EntityConfigBundle\DependencyInjection\EntityConfigContainer', + array($scope, $config) + ); + $container->setDefinition('oro_entity_config.entity_config.' . $scope, $definition); + } + } + } + } + } + + /** + * @param ContainerBuilder $container + * @param $config + * @throws RuntimeException + */ + protected function configCache(ContainerBuilder $container, $config) + { + $cacheDir = $container->getParameterBag()->resolveValue($config['cache_dir']); + + $fs = new Filesystem(); + $fs->remove($cacheDir); + + if (!is_dir($cacheDir)) { + if (false === @mkdir($cacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); + } + } + $container->setParameter('oro_entity_config.cache_dir', $cacheDir); + + $configCacheDir = $cacheDir . '/config'; + if (!is_dir($configCacheDir)) { + if (false === @mkdir($configCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create config cache directory "%s".', $configCacheDir)); + } + } + $container->setParameter('oro_entity_config.cache_dir.config', $configCacheDir); + + $annotationCacheDir = $cacheDir . '/annotation'; + if (!is_dir($annotationCacheDir)) { + if (false === @mkdir($annotationCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create annotation cache directory "%s".', $annotationCacheDir)); + } + } + $container->setParameter('oro_entity_config.cache_dir.annotation', $annotationCacheDir); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php new file mode 100644 index 00000000000..20e597830c1 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php @@ -0,0 +1,34 @@ +container = $container; + $this->serviceId = $serviceId; + } + + public function getService() + { + $this->init(); + + return $this->service; + } + + protected function init() + { + if ($this->service === null) { + $this->service = $this->container->get($this->serviceId); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php new file mode 100644 index 00000000000..f3e391927cb --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php @@ -0,0 +1,218 @@ +values->clear(); + + foreach ($values as $value) { + $this->addValue($value); + } + + return $this; + } + + /** + * @param ConfigValue $value + * @return $this + */ + public function addValue($value) + { + $this->values->add($value); + + return $this; + } + + /** + * @param string $mode + * @return $this + */ + public function setMode($mode) + { + $this->mode = $mode; + + return $this; + } + + /** + * @return string + */ + public function getMode() + { + return $this->mode; + } + + /** + * @param callable $filter + * @return array|ArrayCollection|ConfigValue[] + */ + public function getValues(\Closure $filter = null) + { + return $filter ? $this->values->filter($filter) : $this->values; + } + + /** + * @param $code + * @param $scope + * @return ConfigValue + */ + public function getValue($code, $scope) + { + $values = $this->getValues(function (ConfigValue $value) use ($code, $scope) { + return ($value->getScope() == $scope && $value->getCode() == $code); + }); + + return $values->first(); + } + + /** + * @param \DateTime $created + * @return $this + */ + public function setCreated($created) + { + $this->created = $created; + + return $this; + } + + /** + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * @param \DateTime $updated + * @return $this + */ + public function setUpdated($updated) + { + $this->updated = $updated; + + return $this; + } + + /** + * @return \DateTime + */ + public function getUpdated() + { + return $this->updated; + } + + /** + * @param $scope + * @param array $values + * @param array $serializableValues + */ + public function fromArray($scope, array $values, array $serializableValues = array()) + { + foreach ($values as $code => $value) { + $serializable = isset($serializableValues[$code]) && (bool)$serializableValues[$code]; + + if (!$serializable && is_bool($value)) { + $value = (int)$value; + } + + if (!$serializable && !is_string($value)) { + $value = (string)$value; + } + + if ($configValue = $this->getValue($code, $scope)) { + $configValue->setValue($value); + } else { + + $configValue = new ConfigValue($code, $scope, $value, $serializable); + + if ($this instanceof ConfigEntity) { + $configValue->setEntity($this); + } else { + $configValue->setField($this); + } + + $this->addValue($configValue); + } + } + } + + /** + * @param $scope + * @return array + */ + public function toArray($scope) + { + $values = $this->getValues(function (ConfigValue $value) use ($scope) { + return $value->getScope() == $scope; + }); + + $result = array(); + foreach ($values as $value) { + $result[$value->getCode()] = $value->getValue(); + } + + return $result; + } + + /** + * @ORM\PrePersist + */ + public function prePersist() + { + $this->created = $this->updated = new \DateTime('now', new \DateTimeZone('UTC')); + } + + /** + * @ORM\PreUpdate + */ + public function preUpdate() + { + $this->updated = new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php new file mode 100644 index 00000000000..4e26aff3d8e --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php @@ -0,0 +1,122 @@ +className = $className; + $this->fields = new ArrayCollection(); + $this->values = new ArrayCollection(); + $this->mode = self::MODE_VIEW_DEFAULT; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $className + * @return $this + */ + public function setClassName($className) + { + $this->className = $className; + + return $this; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * @param ConfigField[] $fields + * @return $this + */ + public function setFields($fields) + { + $this->fields = $fields; + + return $this; + } + + /** + * @param ConfigField $field + * @return $this + */ + public function addField($field) + { + $field->setEntity($this); + $this->fields->add($field); + + return $this; + } + + /** + * @param callable $filter + * @return ConfigField[]|ArrayCollection + */ + public function getFields(\Closure $filter = null) + { + return $filter ? $this->fields->filter($filter) : $this->fields; + } + + /** + * @param $code + * @return ConfigField + */ + public function getField($code) + { + $fields = $this->getFields(function (ConfigField $field) use ($code) { + return $field->getCode() == $code; + }); + + return $fields->first(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php new file mode 100644 index 00000000000..d6496e21888 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php @@ -0,0 +1,127 @@ +code = $code; + $this->type = $type; + $this->values = new ArrayCollection; + $this->mode = self::MODE_VIEW_DEFAULT; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @param string $type + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param ConfigEntity $entity + * @return $this + */ + public function setEntity($entity) + { + $this->entity = $entity; + + return $this; + } + + /** + * @return ConfigEntity + */ + public function getEntity() + { + return $this->entity; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php new file mode 100644 index 00000000000..bd2fc61b449 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php @@ -0,0 +1,137 @@ +diffs = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param \DateTime $loggedAt + * @return $this + */ + public function setLoggedAt($loggedAt) + { + $this->loggedAt = $loggedAt; + + return $this; + } + + /** + * @return \DateTime + */ + public function getLoggedAt() + { + return $this->loggedAt; + } + + /** + * @param User $user + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @param ConfigLogDiff[] $diffs + * @return $this + */ + public function setDiffs($diffs) + { + $this->diffs = $diffs; + + return $this; + } + + /** + * @param ConfigLogDiff $diff + * @return $this + */ + public function addDiff(ConfigLogDiff $diff) + { + if (!$this->diffs->contains($diff)) { + $diff->setLog($this); + $this->diffs->add($diff); + } + + return $this; + } + + /** + * @return ConfigLogDiff[]|ArrayCollection + */ + public function getDiffs() + { + return $this->diffs; + } + + /** + * @ORM\PrePersist + */ + public function prePersist() + { + $this->loggedAt = new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php new file mode 100644 index 00000000000..72e459184c9 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php @@ -0,0 +1,158 @@ +id; + } + + /** + * @param array[] $configs + * @return $this + */ + public function setDiff(array $configs) + { + $this->diff = serialize($configs); + + return $this; + } + + /** + * @return array[] + */ + public function getDiff() + { + return unserialize($this->diff); + } + + /** + * @param string $className + * @return $this + */ + public function setClassName($className) + { + $this->className = $className; + + return $this; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * @param string $fieldName + * @return $this + */ + public function setFieldName($fieldName) + { + $this->fieldName = $fieldName; + + return $this; + } + + /** + * @return string + */ + public function getFieldName() + { + return $this->fieldName; + } + + /** + * @param string $scope + * @return $this + */ + public function setScope($scope) + { + $this->scope = $scope; + + return $this; + } + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @param ConfigLog $log + * @return $this + */ + public function setLog($log) + { + $this->log = $log; + + return $this; + } + + /** + * @return ConfigLog + */ + public function getLog() + { + return $this->log; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php new file mode 100644 index 00000000000..a1c8212b4f4 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php @@ -0,0 +1,210 @@ +code = $code; + $this->scope = $scope; + $this->serializable = $serializable; + + $this->setValue($value); + } + + /** + * Get id + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set code + * @param string $code + * @return ConfigValue + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * Get code + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @param string $scope + * @return ConfigValue + */ + public function setScope($scope) + { + $this->scope = $scope; + + return $this; + } + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * Set data + * @param string $value + * @return ConfigValue + */ + public function setValue($value) + { + $this->value = $this->serializable ? serialize($value) : $value; + + return $this; + } + + /** + * Get data + * @return string + */ + public function getValue() + { + return $this->serializable ? unserialize($this->value) : $this->value; + } + + /** + * @param ConfigEntity $entity + * @return $this + */ + public function setEntity($entity) + { + $this->entity = $entity; + + return $this; + } + + /** + * @return ConfigEntity + */ + public function getEntity() + { + return $this->entity; + } + + /** + * @param ConfigField $field + * @return $this + */ + public function setField($field) + { + $this->field = $field; + + return $this; + } + + /** + * @return ConfigField + */ + public function getField() + { + return $this->field; + } + + public function toArray() + { + return array( + 'code' => $this->code, + 'scope' => $this->scope, + 'value' => $this->serializable ? unserialize($this->value) : $this->value, + 'serializable' => $this->serializable + ); + } + + /** + * @param boolean $serializable + * @return $this + */ + public function setSerializable($serializable) + { + $this->serializable = $serializable; + + return $this; + } + + /** + * @return boolean + */ + public function getSerializable() + { + return $this->serializable; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php b/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php new file mode 100644 index 00000000000..54faacd19a0 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php @@ -0,0 +1,39 @@ +createQueryBuilder('cl') + ->select('cld.diff as diff, cld.className as className, cld.fieldName as fieldName, cld.scope as scope') + ->addSelect('cl.id as logId') + ->addSelect('u.username as username') + ->leftJoin('cl.diffs', 'cld') + ->leftJoin('cl.user', 'u'); + + if (!$className) { + return $qb; + } + + $qb->setParameter('className', $className); + $qb->andWhere('cld.className = :className'); + + if ($fieldName) { + $qb->setParameter('fieldName', $fieldName); + $qb->andWhere('cld.fieldName = :fieldName'); + } else { + $qb->andWhere('cld.fieldName IS NULL'); + } + + if ($scope) { + $qb->setParameter('scope', $scope); + $qb->andWhere('cld.scope = :scope'); + } + + return $qb; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/Events.php b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php new file mode 100644 index 00000000000..3d159cdf4fe --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php @@ -0,0 +1,16 @@ +configManager = $configManager; + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php new file mode 100644 index 00000000000..62c8bb99fba --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php @@ -0,0 +1,47 @@ +className = $className; + $this->configManager = $configManager; + } + + /** + * @return EntityConfig + */ + public function getClassName() + { + return $this->className; + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php new file mode 100644 index 00000000000..ed319606f60 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php @@ -0,0 +1,77 @@ +className = $className; + $this->fieldName = $fieldName; + $this->fieldType = $fieldType; + $this->configManager = $configManager; + } + + /** + * @return EntityConfig + */ + public function getClassName() + { + return $this->className; + } + + /** + * @return string + */ + public function getFieldName() + { + return $this->fieldName; + } + + /** + * @return string + */ + public function getFieldType() + { + return $this->fieldType; + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php new file mode 100644 index 00000000000..b4b892b72ef --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php @@ -0,0 +1,44 @@ +config = $config; + $this->configManager = $configManager; + } + + /** + * @return ConfigInterface + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Exception/AnnotationException.php b/src/Oro/Bundle/EntityConfigBundle/Exception/AnnotationException.php new file mode 100644 index 00000000000..7b10e3b2bcb --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Exception/AnnotationException.php @@ -0,0 +1,7 @@ +configManager = $configManager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + FormEvents::POST_SUBMIT => 'postSubmit' + ); + } + + /** + * @param FormEvent $event + */ + public function postSubmit(FormEvent $event) + { + $options = $event->getForm()->getConfig()->getOptions(); + $className = $options['class_name']; + $fieldName = isset($options['field_name']) ? $options['field_name'] : null; + $data = $event->getData(); + + foreach ($this->configManager->getProviders() as $provider) { + if (isset($data[$provider->getScope()])) { + if ($fieldName) { + $config = $provider->getFieldConfig($className, $fieldName); + } else { + $config = $provider->getConfig($className); + } + $config->setValues($data[$provider->getScope()]); + //TODO::look after a EntityConfig changes in configManager + $this->configManager->persist($config); + } + } + + $this->configManager->flush(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php b/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php new file mode 100644 index 00000000000..78b6b446de7 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php @@ -0,0 +1,51 @@ +vars['attr'] = array_merge($view->vars['attr'], array('data-allowedType' => $options['allowed_type'])); + + $types = explode(',', $options['allowed_type']); + $types = array_map('trim', $types); + if (!$fieldType || !in_array($fieldType, $types)) { + $view->vars['attr']['class'] = (isset($view->vars['attr']['class']) ? $view->vars['attr']['class'] : '') . 'hide'; + } + + $view->vars['attr'] = array_merge($view->vars['attr'], array('data-fieldType' => $fieldType)); + } + + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setOptional(array( + 'allowed_type', + 'field_type' + )); + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php new file mode 100644 index 00000000000..1f87fb62186 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php @@ -0,0 +1,74 @@ +configManager = $configManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $className = $options['class_name']; + $fieldId = $options['entity_id']; + + $data = array( + 'id' => $fieldId, + ); + + $builder->add('id', 'hidden'); + + foreach ($this->configManager->getProviders() as $provider) { + if ($provider->getConfigContainer()->hasEntityForm()) { + $builder->add( + $provider->getScope(), + new ConfigType($provider->getConfigContainer()->getEntityItems()), + array( + 'block_config' => (array) $provider->getConfigContainer()->getEntityFormBlockConfig() + ) + ); + $data[$provider->getScope()] = $provider->getConfig($className)->getValues(); + } + } + $builder->setData($data); + + $builder->addEventSubscriber(new ConfigSubscriber($this->configManager)); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setRequired(array('class_name', 'entity_id')); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_config_config_entity_type'; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php new file mode 100644 index 00000000000..b76033f4204 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php @@ -0,0 +1,112 @@ +configManager = $configManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $className = $options['class_name']; + $fieldName = $options['field_name']; + $fieldType = $options['field_type']; + $fieldId = $options['field_id']; + + $data = array( + 'id' => $fieldId, + ); + + $builder->add('id', 'hidden'); + + foreach ($this->configManager->getProviders() as $provider) { + if ($provider->getConfigContainer()->hasFieldForm()) { + $items = $provider->getConfigContainer()->getFieldItems(); + + $defaultValues = $provider->getConfigContainer()->getFieldDefaultValues(); + + $allowedTypes = array_map( + function ($item) use ($fieldType) { + if (isset($item['form']['options']['allowed_type'])) { + return array_map('trim', explode(',', $item['form']['options']['allowed_type'])); + } + + return false; + }, + $items + ); + + foreach ($allowedTypes as $key => $allowedType) { + if (isset($defaultValues[$key]) && is_array($allowedType) && !in_array($fieldType, $allowedType)) { + unset($defaultValues[$key]); + } + } + + foreach ($provider->getConfigContainer()->getFieldRequiredPropertyValues() as $code => $property) { + list($scope, $propertyName) = explode('.', $property['property_path']); + + if ($this->configManager->getProvider($scope)->hasFieldConfig($className, $fieldName)) { + $value = $this->configManager->getProvider($scope)->getFieldConfig($className, $fieldName)->get($propertyName); + if ($value !== null && $value != $property['value']) { + unset($items[$code]); + } + } + } + + $builder->add( + $provider->getScope(), + new ConfigType($items, $fieldType), + array('block_config' => (array) $provider->getConfigContainer()->getFieldFormBlockConfig()) + ); + + $values = $provider->getFieldConfig($className, $fieldName)->getValues(); + + $data[$provider->getScope()] = array_merge($defaultValues, $values); + } + } + + $builder->setData($data); + + $builder->addEventSubscriber(new ConfigSubscriber($this->configManager)); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setRequired( + array('class_name', 'field_type', 'field_name', 'field_id') + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_config_config_field_type'; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php new file mode 100644 index 00000000000..46691c1411f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php @@ -0,0 +1,54 @@ +items = $items; + $this->fieldType = $fieldType; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + foreach ($this->items as $code => $config) { + if (isset($config['form']) && isset($config['form']['type'])) { + $options = isset($config['form']['options']) ? $config['form']['options'] : array(); + + if ($this->fieldType) { + $options['field_type'] = $this->fieldType; + } + $builder->add($code, $config['form']['type'], $options); + } + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_config_config_type'; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php new file mode 100644 index 00000000000..6ac290f8487 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php @@ -0,0 +1,7 @@ +viewMode = $data['viewMode']; + } elseif (isset($data['value'])) { + $this->viewMode = $data['value']; + } + + if (isset($data['routeName'])) { + $this->routeName = $data['routeName']; + } + + if (isset($data['defaultValues'])) { + $this->defaultValues = $data['defaultValues']; + } + + if (!is_array($this->defaultValues)) { + throw new AnnotationException(sprintf('Annotation "Configurable" parameter "defaultValues" expect "array" give "%s"', $this->defaultValues)); + } + + if (!in_array($this->viewMode, array(AbstractConfig::MODE_VIEW_DEFAULT, AbstractConfig::MODE_VIEW_HIDDEN, AbstractConfig::MODE_VIEW_READONLY))) { + throw new AnnotationException(sprintf('Annotation "Configurable" give invalid parameter "viewMode" : "%s"', $this->viewMode)); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php new file mode 100644 index 00000000000..9d6991f88a9 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php @@ -0,0 +1,74 @@ +configurable = $object->configurable; + $this->defaultValues = $object->defaultValues; + $this->routeName = $object->routeName; + $this->viewMode = $object->viewMode; + } + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->configurable, + $this->defaultValues, + $this->routeName, + $this->viewMode, + parent::serialize(), + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($str) + { + list( + $this->configurable, + $this->defaultValues, + $this->routeName, + $this->viewMode, + $parentStr + ) = unserialize($str); + + parent::unserialize($parentStr); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php new file mode 100644 index 00000000000..60a3ff47ea3 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php @@ -0,0 +1,51 @@ +reader = $reader; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass(\ReflectionClass $class) + { + /** @var Configurable $annot */ + if ($annot = $this->reader->getClassAnnotation($class, self::CONFIGURABLE)) { + $metadata = new ConfigClassMetadata($class->getName()); + + $metadata->configurable = true; + $metadata->defaultValues = $annot->defaultValues; + $metadata->routeName = $annot->routeName; + $metadata->viewMode = $annot->viewMode; + + return $metadata; + } + + return null; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php b/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php new file mode 100644 index 00000000000..a239d654c89 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php @@ -0,0 +1,18 @@ +addCompilerPass(new EntityConfigPass); + $container->addCompilerPass(new ServiceProxyPass); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php new file mode 100644 index 00000000000..5401c19ee39 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php @@ -0,0 +1,184 @@ +configManager = $configManager; + $this->configContainer = $configContainer; + $this->scope = $configContainer->getScope(); + } + + /** + * @return EntityConfigContainer + */ + public function getConfigContainer() + { + return $this->configContainer; + } + + /** + * @param $className + * @return EntityConfig + */ + public function getConfig($className) + { + return $this->configManager->getConfig($this->getClassName($className), $this->scope); + } + + /** + * @param $className + * @return bool + */ + public function hasConfig($className) + { + return $this->configManager->hasConfig($this->getClassName($className)); + } + + /** + * @param $className + * @param $code + * @return FieldConfig + */ + public function getFieldConfig($className, $code) + { + return $this->getConfig($className)->getField($code); + } + + /** + * @param $className + * @param $code + * @return FieldConfig + */ + public function hasFieldConfig($className, $code) + { + return $this->hasConfig($className) + ? $this->getConfig($className)->hasField($code) + : false; + } + + /** + * @param $className + * @param array $values + * @param bool $flush + * @return EntityConfig + */ + public function createEntityConfig($className, array $values, $flush = false) + { + $className = $this->getClassName($className); + $entityConfig = new EntityConfig($className, $this->scope); + + foreach ($values as $key => $value) { + $entityConfig->set($key, $value); + } + + $this->configManager->persist($entityConfig); + + if ($flush) { + $this->configManager->flush(); + } + } + + /** + * @param $className + * @param $code + * @param $type + * @param array $values + * @param bool $flush + * @return FieldConfig + */ + public function createFieldConfig($className, $code, $type, array $values = array(), $flush = false) + { + $className = $this->getClassName($className); + $fieldConfig = new FieldConfig($className, $code, $type, $this->scope); + + foreach ($values as $key => $value) { + $fieldConfig->set($key, $value); + } + + $this->configManager->persist($fieldConfig); + + if ($flush) { + $this->configManager->flush(); + } + } + + /** + * @param $entity + * @return string + * @throws RuntimeException + */ + public function getClassName($entity) + { + $className = $entity; + + if ($entity instanceof PersistentCollection) { + $className = $entity->getTypeClass()->getName(); + } elseif (is_object($entity)) { + $className = get_class($entity); + } elseif (is_array($entity) && count($entity) && is_object(reset($entity))) { + $className = get_class(reset($entity)); + } + + if (!is_string($className)) { + throw new RuntimeException('AbstractAdvancedConfigProvider::getClassName expects Object, PersistentCollection array of entities or string'); + } + + return $className; + } + + /** + * @param ConfigInterface $config + */ + public function persist(ConfigInterface $config) + { + $this->configManager->persist($config); + } + + /** + * Flush configs + */ + public function flush() + { + $this->configManager->flush(); + } + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderInterface.php b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderInterface.php new file mode 100644 index 00000000000..6e0a2d9edbe --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderInterface.php @@ -0,0 +1,8 @@ + Doctrine entity instance + + /** @var \Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider $entityAuditProvider */ + $entityAuditProvider = $this->get('oro_entity.config.audit_config_provider'); + +ConfigProvider methods: + + hasConfig({Entity class name}) : checks if entity has config + getConfig({Entity class name}) : return configuration ( EntityConfig(AbstractConfig) instance ) + + hasFieldConfig({Entity class name}, {Field code}) : checks if field of entity has config + getFieldConfig({Entity class name}, {Field code}) : return configuration for specified field ( FieldConfig(AbstractConfig) instance ) + + AbstractConfig->is({parameter}) : check if parameter exists or equal to TRUE, return boolean + AbstractConfig->has({parameter}) : check if parameter exists, return boolean + + AbstractConfig->get({parameter}, {strict = FALSE}) : return parameters + - if strict == TRUE and parameters NOT exists will be Exception + - if strict == FALSE and parameters NOT exists will return NULL + + AbstractConfig->set({parameter}, {value}) : set parameter and return AbstractConfig + +Simple usage example: + if ($entityAuditProvider->hasConfig(get_class($entity))) { + $audit_enabled = $entityAuditProvider->getConfig(get_class($entity))->is('auditable'); + } diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/doc/configuration.md b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/configuration.md new file mode 100644 index 00000000000..7f61bc1052c --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/configuration.md @@ -0,0 +1,115 @@ +Config(yml) Example +==================== + +Configuration YAML should be placed in [BundleName]\Resources\config\entity_config.yml + +oro_entity_config: + entity: # scope name + entity: # config block for Entity instance + + form: # config block for Entity form ( FormBundle ) + block_config: # + entity: # block name + priority: 20 # ability to sort block(s) + title: 'Entity Config' # form block title + subblocks: # subblock(s) configuration + base: ~ + + items: # config block for Entity properties + + label: # property code + options: + priority: 20 # default sort order (will be used in grid and form if not specified) + + grid: # config for GridBundle (same as in DatagridManager) + type: string + label: 'Label' + filter_type: oro_grid_orm_string + required: true + sortable: true + filterable: true + show_filter: true + form: # Entity update form + type: text # field type + options: + block: entity # field will be rendered in this block ( specified in entity.form.block_config) + required: true # is field required or not + + field: # config block for Entity's Field + items: # block of entity + + label: ~ # same as entity.items.label + + + +Below just an example of scope configurations: + + audit: + entity: + items: + auditable: + options: + priority: 60 + grid: + type: boolean + label: 'Auditable' + filter_type: oro_grid_orm_boolean + required: true + sortable: true + filterable: true + show_filter: false + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: entity + label: 'Auditable' + field: + items: + auditable: + options: + priority: 60 + serializable: true + grid: + type: boolean + label: 'Auditable' + filter_type: oro_grid_orm_boolean + required: true + sortable: true + filterable: true + show_filter: false + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: entity + label: 'Auditable' + datagrid: + field: + form: + block_config: + datagrid: + title: 'Datagrid Config' + subblocks: + base: ~ + items: + is_searchable: + options: + default_value: false + grid: + type: boolean + label: 'Datagrid search' + filter_type: oro_grid_orm_boolean + required: true + sortable: true + filterable: true + show_filter: false + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: datagrid + label: "Datagrid search" diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/doc/index.md b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/index.md new file mode 100644 index 00000000000..42a7c9d70ab --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/index.md @@ -0,0 +1,19 @@ +Configurable Entity Example +==================== + +For ability to configure an Entity it should be marked as Configurable. +For example: + + #entity_field_meta.value + length: 40 + nullable: true + default_value: '' + + constraint: ~ + + datagrid: + is_searchable: true + is_sortable: false + + ui: + label: #entity_field_meta.code + en: Firstname #entity_field_meta.key -> #entity_field_meta.value + fr: Firstname FR + de: Firstname DE + + description: Firstname Text + scope: [] + + default: ~ + + name: + ui: + label: Customer Name + icon_class: customer-name + description: Custom User Text + scope: [] + + datagrid: + is_searchable: false + is_sortable: false diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig new file mode 100644 index 00000000000..adb1e357d01 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig @@ -0,0 +1,14 @@ +{% set gridId = "entity-grid-" ~ random() %} + +{% block head_script %} + {% include 'OroGridBundle:Include:javascript.html.twig' with {'datagridView': datagrid, 'selector': '#' ~ gridId} %} + {% include 'OroGridBundle:Include:stylesheet.html.twig' %} +{% endblock %} + +{% block page_container %} +
    + {% block content %} +
    + {% endblock %} +
    +{% endblock %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig new file mode 100644 index 00000000000..53dd9d28250 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig @@ -0,0 +1,22 @@ +
      +{% for val in value %} + {% if is_entity %} + {% set items = config_manager.getProvider(val.scope).getConfigContainer().getEntityItems() %} + {% else %} + {% set items = config_manager.getProvider(val.scope).getConfigContainer().getFieldItems() %} + {% endif %} + + {% for key, data in val.diff if (is_entity and val.fieldName() == null) or (is_entity == false and val.fieldName() == field_name) %} + {% if items[key]['form']['options']['label'] is defined %} + {% set label = items[key]['form']['options']['label'] %} + {% elseif items[key]['options']['label'] is defined %} + {% set label = items[key]['options']['label'] %} + {% else %} + {% set label = key|capitalize|replace('_',' ') %} + {% endif %} +
    • + {{ label }}: {{ data[0]|default('N/A') }} {{ data[1] }} +
    • + {% endfor %} +{% endfor %} +
    diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig new file mode 100644 index 00000000000..3bd619383c5 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig @@ -0,0 +1,51 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} + +{% set title = 'Update Entity' %} +{% oro_title_set({params : {"%username%": 'User' }}) %} + +{% set formAction = path('oro_entityconfig_field_update', {id: field.id}) %} + +{% set entityClass = field.entity.className|replace('\\', '_') %} +{% set audit_title = entity_config.get('label') %} +{% set audit_path = 'oro_entityconfig_audit_field' %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_view', {id: field.entity.id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity' : 'entity', + 'indexPath' : path('oro_entityconfig_index'), + 'indexLabel' : 'Entities', + 'entityTitle' : field.id ? field_config.get('label')|default(field.code|capitalize) : 'New Field'|trans, + 'additional' : [ + { + 'indexPath' : path('oro_entityconfig_view', {id: field.entity.id}), + 'indexLabel' : entity_config.get('label')|default('N/A'), + }, + ] + }%} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} + {{ parent() }} +{% endblock stats %} + +{% block content_data %} + {% set id = 'configfield-update' %} + {% set dataBlocks = form_data_blocks(form) %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/index.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/index.html.twig new file mode 100644 index 00000000000..99319bbad62 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'OroUIBundle:actions:index.html.twig' %} +{% import 'OroUIBundle::macros.html.twig' as UI %} + +{% set gridId = 'entityconfig-grid' %} + +{% block content %} + {% set pageTitle = 'Entities' %} + + {% set buttons = [] %} + {% for button in buttonConfig %} + {% set buttons = buttons|merge([UI.addButton({ + 'path': path(button.route, button.args|default({})), + 'title': button.title|default(button.name), + 'label': button.name, + 'aClass': button.aClass|default('') + })]) %} + {% endfor %} + + {{ parent() }} +{% endblock %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig new file mode 100644 index 00000000000..6489dc833d4 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig @@ -0,0 +1,44 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} +{% oro_title_set({params : {"%entityName%": entity_config.get('label')|default('N/A') }}) %} + +{% set title = 'Update Entity' %} +{% set formAction = path('oro_entityconfig_update', {id: entity.id}) %} + +{% set entityClass = entity.classname|replace('\\', '_') %} +{% set audit_title = entity_config.get('label')|default('N/A') %} +{% set audit_path = 'oro_entityconfig_audit' %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_view', {id: entity.id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': entity_config.get('label')|default('N/A'), + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} + {{ parent() }} +{% endblock stats %} + +{% block content_data %} + {% set id = 'configentity-update' %} + {% set dataBlocks = form_data_blocks(form) %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig new file mode 100644 index 00000000000..43470774cae --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig @@ -0,0 +1,103 @@ +{% extends 'OroUIBundle:actions:view.html.twig' %} +{% set gridId = 'fields-grid' %} +{% oro_title_set({params : {"%entityName%": entity_config.get('label')|default('N/A') }}) %} + +{% set entityClass = entity.classname|replace('\\', '_') %} +{% set audit_title = entity_config.get('label')|default(entity_name) %} +{% set audit_path = 'oro_entityconfig_audit' %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_index'), 'iClass' : 'icon-chevron-left ', 'title' : 'Back', 'label' : 'Back'}) }} + {% if resource_granted('oro_user_user_update') %} + {{ UI.button({'path' : path('oro_entityconfig_update', { id: entity.id }), 'iClass' : 'icon-edit ', 'title' : 'Edit user', 'label' : 'Edit'}) }} + {% endif %} + + {% for button in button_config %} + {{ UI.button({ + 'path' : button.void is defined ? [ 'javascript:void(0);//', path(button.route, button.args|default({})) ]|join : path(button.route, button.args|default({})), + 'iClass' : button.iClass|default(''), + 'aClass' : button.aClass|default(''), + 'title' : button.title|default(button.name), + 'label' : button.name + }) }} + {% endfor %} +{% endblock navButtons %} + +{% block head_script %} + {{ parent() }} + + {% placeholder prepare_grid with {'datagrid': entity_fields, 'selector': '#' ~ gridId, 'parameters': ''} %} +{% endblock %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': entity, + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': entity_config.get('label')|default(entity_name), + }%} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} +
  • {{ 'Created'|trans }}: {{ UI.Time(entity.created) }}
  • +
  • {{ 'Updated'|trans }}: {{ UI.Time(entity.updated) }}
  • + {% if link %} +
  • {{ ['', 'Number of records'|trans, ': ', entity_count|default(0) , '']|join|raw }}
  • + {% else %} +
  • {{ ['', 'Number of records'|trans, ': ', entity_count|default(0) , '']|join|raw }}
  • + {% endif %} +{% endblock stats%} + +{% block content_data %} + {% set id = 'entityconfig-view' %} + {% set unique_keys = [] %} + {% for key in unique_key.keys|default %} + {% set unique_keys = unique_keys|merge([UI.attibuteRow(key.name, key.key|join(', '))])%} + {% endfor %} + + {% set general_fields = [ + UI.attibuteRow('Icon', entity_config.get('icon')), + UI.attibuteRow('Label', entity_config.get('label')), + UI.attibuteRow('Plural Label', entity_config.get('plural_label')), + UI.attibuteRow('Type', entity_extend.get('owner')), + UI.attibuteRow('Description', entity_config.get('description')), + + UI.attibuteRow('Name', entity_name), + UI.attibuteRow('Module', module_name), + ]%} + + {#{% for key,property in entity_config.getValues %}#} + {#{% set general_fields = general_fields|merge([UI.attibuteRow(key|replace('_',' ')|capitalize, property)])%}#} + {#{% endfor %}#} + {#{% set general_fields = general_fields|merge([UI.attibuteRow('Type', entity_extend.get('type'))])%}#} + {#{% for key,property in entity_extend.getValues %}#} + {#{% set general_fields = general_fields|merge([UI.attibuteRow(key|replace('_',' ')|capitalize, property)])%}#} + {#{% endfor %}#} + + {% set data = { + 'dataBlocks': [ + { + 'title': 'General', + 'class': 'active', + 'subblocks': [ + {'title': 'General Information', 'data': general_fields}, + {'title': 'Unique Keys', 'data': unique_keys} + ] + }, + { + 'title': 'Fields', + 'subblocks': [ + { + 'title': '', + 'useSpan': false, + 'data': [UI.gridBlock(gridId)] + } + ] + } + ] + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php new file mode 100644 index 00000000000..3e2b8bcb46c --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php @@ -0,0 +1,40 @@ +em = $this->_getTestEntityManager(); + $this->em->getConfiguration()->setEntityNamespaces(array( + 'OroEntityConfigBundle' => 'Oro\\Bundle\\EntityConfigBundle\\Entity', + 'Fixture' => 'Oro\\Bundle\\EntityConfigBundle\\Tests\\Unit\\Fixture' + )); + $this->em->getConfiguration()->setMetadataDriverImpl($metadataDriver); + + $schema = $this->getMockBuilder('Doctrine\Tests\Mocks\SchemaManagerMock') + ->disableOriginalConstructor() + ->getMock(); + + $schema->expects($this->any())->method('listTableNames')->will($this->returnValue(array('oro_config_entity'))); + $this->em->getConnection()->getDriver()->setSchemaManager($schema); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php new file mode 100644 index 00000000000..cf298811e9b --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php @@ -0,0 +1,62 @@ +cacheDir = sys_get_temp_dir() . '/__phpunit__config_file_cache'; + mkdir($this->cacheDir); + + $this->testConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'test'); + $this->fileCache = new FileCache($this->cacheDir); + } + + protected function tearDown() + { + rmdir($this->cacheDir); + } + + public function testCache() + { + $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $this->assertEquals(null, $result); + + $this->fileCache->putConfigInCache($this->testConfig); + + $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $this->assertEquals($this->testConfig, $result); + + $this->fileCache->removeConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $this->assertEquals(null, $result); + } + + public function testExceptionNotFoundDirectory() + { + $cacheDir = '/__phpunit__config_file_cache_wrong'; + $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" does not exist.', $cacheDir)); + $this->fileCache = new FileCache($cacheDir); + } + + public function testExceptionNotWritableDirectory() + { + $cacheDir = '/'; + $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" is not writable.', $cacheDir)); + $this->fileCache = new FileCache($cacheDir); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php new file mode 100644 index 00000000000..4b6596695de --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php @@ -0,0 +1,43 @@ +entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'testScope'); + } + + public function testSetClassName() + { + $this->entityConfig->setClassName('testClass'); + $this->assertEquals('testClass', $this->entityConfig->getClassName()); + } + + public function testField() + { + $fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); + + $this->entityConfig->addField($fieldConfig); + + $this->assertEquals(true, $this->entityConfig->hasField('testField')); + $this->assertEquals($fieldConfig, $this->entityConfig->getField('testField')); + + $fieldConfig2 = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField2', 'string', 'testScope'); + + $this->entityConfig->setFields(array($fieldConfig2)); + + $this->assertEquals(true, $this->entityConfig->hasField('testField2')); + $this->assertEquals($fieldConfig2, $this->entityConfig->getField('testField2')); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php new file mode 100644 index 00000000000..54c719096a6 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php @@ -0,0 +1,67 @@ +fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); + } + + public function testGetConfig() + { + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->fieldConfig->getClassName()); + $this->assertEquals('testField', $this->fieldConfig->getCode()); + $this->assertEquals('string', $this->fieldConfig->getType()); + $this->assertEquals('testScope', $this->fieldConfig->getScope()); + } + + public function testSetConfig() + { + $this->fieldConfig->setClassName('testClass'); + $this->fieldConfig->setCode('testField2'); + $this->fieldConfig->setType('datetime'); + + $this->assertEquals('testClass', $this->fieldConfig->getClassName()); + $this->assertEquals('testField2', $this->fieldConfig->getCode()); + $this->assertEquals('datetime', $this->fieldConfig->getType()); + } + + public function testValueConfig() + { + $values = array('firstKey' => 'firstValue', 'secondKey' => 'secondValue'); + $this->fieldConfig->setValues($values); + + $this->assertEquals($values, $this->fieldConfig->getValues()); + + $this->assertEquals('firstValue', $this->fieldConfig->get('firstKey')); + $this->assertEquals('secondValue', $this->fieldConfig->get('secondKey')); + + $this->assertEquals(true, $this->fieldConfig->is('secondKey')); + + $this->assertEquals(true, $this->fieldConfig->has('secondKey')); + $this->assertEquals(false, $this->fieldConfig->has('thirdKey')); + + $this->assertEquals(null, $this->fieldConfig->get('thirdKey')); + + $this->fieldConfig->set('secondKey', 'secondValue2'); + $this->assertEquals('secondValue2', $this->fieldConfig->get('secondKey')); + + $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + $this->fieldConfig->get('thirdKey', true); + } + + public function testSerialize() + { + $this->assertEquals($this->fieldConfig, unserialize(serialize($this->fieldConfig))); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php new file mode 100644 index 00000000000..4a7776d7b29 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php @@ -0,0 +1,265 @@ +getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + + $this->securityContext = $this->getMockForAbstractClass( + 'Symfony\Component\Security\Core\SecurityContextInterface' + ); + $token = $this->getMockForAbstractClass( + 'Symfony\Component\Security\Core\Authentication\Token\TokenInterface' + ); + + $token->expects($this->any())->method('getUser')->will($this->returnValue($user)); + + $this->securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); + + $this->securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') + ->disableOriginalConstructor() + ->getMock(); + $this->securityProxy->expects($this->any())->method('getService')->will($this->returnValue($this->securityContext)); + + $this->initConfigManager(); + + $this->configCache = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Cache\FileCache') + ->disableOriginalConstructor() + ->getMock(); + + $this->configCache->expects($this->any())->method('putConfigInCache')->will($this->returnValue(null)); + + $this->provider = new ConfigProvider($this->configManager, new EntityConfigContainer('test', array())); + } + + public function testGetConfigFoundConfigEntity() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); + $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + } + + public function testGetConfigNotFoundConfigEntity() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); + $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + } + + public function testGetConfigRuntimeException() + { + $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + $this->configManager->getConfig(self::NO_CONFIGURABLE_ENTITY, 'test'); + } + + public function testGetConfigNotFoundCache() + { + $this->configCache->expects($this->any())->method('loadConfigFromCache')->will($this->returnValue(null)); + + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); + + $this->configManager->setCache($this->configCache); + $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + } + + public function testGetConfigFoundCache() + { + $entityConfig = new EntityConfig(self::DEMO_ENTITY, 'test'); + $this->configCache->expects($this->any())->method('loadConfigFromCache')->will($this->returnValue($entityConfig)); + + $this->configManager->setCache($this->configCache); + $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + } + + public function testHasConfig() + { + $this->assertEquals(true, $this->configManager->hasConfig(self::DEMO_ENTITY, 'test')); + } + + public function testGetEventDispatcher() + { + $this->assertInstanceOf('Symfony\Component\EventDispatcher\EventDispatcher', $this->configManager->getEventDispatcher()); + } + + public function testClearCache() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); + $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + + $this->configCache->expects($this->any())->method('removeConfigFromCache')->will($this->returnValue(null)); + $this->configManager->setCache($this->configCache); + $this->configManager->addProvider($this->provider); + + $this->configManager->clearCache($meta->getName()); + } + + public function testAddAndGetProvider() + { + $this->configManager->addProvider($this->provider); + + $providers = $this->configManager->getProviders(); + $provider = $this->configManager->getProvider('test'); + + $this->assertEquals(array('test' => $this->provider), $providers); + $this->assertEquals($this->provider, $provider); + } + + public function testInitConfigByDoctrineMetadata() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); + + $this->configManager->addProvider($this->provider); + + $this->configManager->initConfigByDoctrineMetadata($this->em->getClassMetadata(self::DEMO_ENTITY)); + } + + public function testPersist() + { + $config = new EntityConfig(self::DEMO_ENTITY, 'test'); + $config->addField(new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test')); + + $this->configManager->persist($config); + } + + public function testRemove() + { + $config = new EntityConfig(self::DEMO_ENTITY, 'test'); + $config->addField(new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test')); + + $this->configManager->remove($config); + } + + public function testFlush() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + + $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->em->expects($this->any())->method('flush')->will($this->returnValue(null)); + $this->em->expects($this->any())->method('getRepository') + ->will($this->returnValue(new NotFoundEntityConfigRepository($this->em, $meta))); + + $this->initConfigManager(); + + $this->configManager->addProvider($this->provider); + + $this->configCache->expects($this->any())->method('removeConfigFromCache')->will($this->returnValue(null)); + $this->configManager->setCache($this->configCache); + + $configEntity = new EntityConfig(self::DEMO_ENTITY, 'test'); + $configField = new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test'); + + $this->configManager->persist($configEntity); + $this->configManager->persist($configField); + + $this->configManager->flush(); + } + + public function testChangeSet() + { + $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); + $config = $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); + $configField = $config->getFields()->first(); + + $configField->set('test_field_value1', 'test_field_value1_new'); + + $config->set('test_value', 'test_value_new'); + $config->set('test_value1', 'test_value1_new'); + + $config->set('test_value_serializable', array('test_value' => 'test_value_new')); + + $this->configManager->calculateConfigChangeSet($config); + $this->configManager->calculateConfigChangeSet($configField); + + $result = array( + 'test_value' => array('test_value_origin', 'test_value_new'), + 'test_value_serializable' => array( + array('test_value' => 'test_value_origin'), + array('test_value' => 'test_value_new') + ), + 'test_value1' => array(null, 'test_value1_new'), + ); + + $this->assertEquals($result, $this->configManager->getConfigChangeSet($config)); + + $this->assertEquals(array(spl_object_hash($config) => $config), $this->configManager->getUpdatedEntityConfig()); + $this->assertEquals(array(), $this->configManager->getUpdatedEntityConfig('test1')); + + $this->assertEquals(array(spl_object_hash($configField) => $configField), $this->configManager->getUpdatedFieldConfig()); + $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig('test1')); + $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig(null, 'WrongClass')); + } + + protected function initConfigManager() + { + $metadataFactory = new MetadataFactory(new AnnotationDriver(new AnnotationReader)); + + $this->emProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') + ->disableOriginalConstructor() + ->getMock(); + $this->emProxy->expects($this->any())->method('getService')->will($this->returnValue($this->em)); + + $this->configManager = new ConfigManager($metadataFactory, new EventDispatcher, $this->emProxy, $this->securityProxy); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php new file mode 100644 index 00000000000..62d805861ad --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php @@ -0,0 +1,100 @@ + array( + 0 => array('scope' => 'datagrid') + )); + + /** Setup */ + protected function setup() + { + $this->compiler = new EntityConfigPass(); + $this->builder = new ContainerBuilder(); + } + + public function testProcess() + { + $this->setDefinitions(); + + $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $containerMock->expects($this->any()) + ->method('findTaggedServiceIds') + ->with('oro_entity_config.provider') + ->will($this->returnValue($this->config)); + + $containerMock->expects($this->any()) + ->method('hasDefinition') + ->with('oro_entity_config.entity_config.datagrid') + ->will($this->returnValue(true)); + + $containerMock->expects($this->any()) + ->method('getDefinition') + ->will($this->returnValue($this->builder->getDefinition('oro_grid.config.datagrid_config_provider'))); + + $compilerPass = new EntityConfigPass(); + $compilerPass->process($containerMock); + } + + public function testWarning() + { + $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $containerMock->expects($this->any()) + ->method('findTaggedServiceIds') + ->with('oro_entity_config.provider') + ->will($this->returnValue(array('oro_entity_config.provider' => array()))); + + $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + + $compilerPass = new EntityConfigPass(); + $compilerPass->process($containerMock); + + } + + public function testException() + { + $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $containerMock->expects($this->any()) + ->method('findTaggedServiceIds') + ->with('oro_entity_config.provider') + ->will($this->returnValue($this->config)); + + $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + + $compilerPass = new EntityConfigPass(); + $compilerPass->process($containerMock); + } + + protected function setDefinitions() + { + $defRegistry_0 = new Definition('Oro\Bundle\EntityConfigBundle\ConfigManager'); + $defRegistry_1 = new Definition('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider'); + $definitions = array( + 'oro_entity_config.config_manager' => $defRegistry_0, + 'oro_grid.config.datagrid_config_provider' => $defRegistry_1 + ); + + $this->builder->setDefinitions($definitions); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 00000000000..1d642e1c2dd --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,27 @@ +getConfigTreeBuilder(); + $this->assertInstanceOf('Symfony\Component\Config\Definition\Builder\TreeBuilder', $builder); + + /** @var ArrayNode $rootNode */ + $rootNode = $builder->buildTree(); + $this->assertInstanceOf('Symfony\Component\Config\Definition\ArrayNode', $rootNode); + + $children = $rootNode->getChildren(); + $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $children['cache_dir']); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php new file mode 100644 index 00000000000..a2ccca275d6 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php @@ -0,0 +1,77 @@ +config = Yaml::parse(__DIR__ . '/../Fixture/entity_config.yml'); + $this->config = reset($this->config['oro_entity_config']); + + $scope = key($this->config); + + $this->container = new EntityConfigContainer($scope, $this->config); + } + + public function testContainer() + { + $this->assertEquals(key($this->config), $this->container->getScope()); + + $this->assertEquals($this->config['field']['items'], $this->container->getFieldItems()); + + $this->assertEquals($this->config, $this->container->getConfig()); + + $this->assertTrue($this->container->hasEntityForm()); + $this->assertTrue($this->container->hasFieldForm()); + + $this->assertEquals( + $this->config['field']['form'], + $this->container->getFieldFormConfig() + ); + + $this->assertEquals( + $this->config['entity']['grid_action'], + $this->container->getEntityGridActions() + ); + + $this->assertEquals( + $this->config['entity']['layout_action'], + $this->container->getEntityLayoutActions() + ); + + $this->assertEquals( + $this->config['field']['grid_action'], + $this->container->getFieldGridActions() + ); + + $this->assertEquals( + $this->config['field']['layout_action'], + $this->container->getFieldLayoutActions() + ); + + $this->assertEquals( + $this->config['entity']['form']['block_config'], + $this->container->getEntityFormBlockConfig() + ); + + $this->assertEquals( + array('enabled' => true), + $this->container->getEntityDefaultValues() + ); + + $this->assertEquals( + array('enabled' => true, 'is_searchable' => false, 'is_filtrableble' => false), + $this->container->getFieldDefaultValues() + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php new file mode 100644 index 00000000000..b9f9c37c18e --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -0,0 +1,165 @@ +configEntity = new ConfigEntity(); + $this->configField = new ConfigField(); + $this->configValue = new ConfigValue(); + } + + public function testProperties() + { + /** test ConfigEntity */ + $this->assertNull($this->configEntity->getClassName()); + $this->assertEmpty($this->configEntity->getId()); + + $this->assertNull($this->configEntity->getCreated()); + $this->configEntity->setCreated(new \DateTime('2013-01-01')); + $this->assertEquals('2013-01-01', $this->configEntity->getCreated()->format('Y-m-d')); + + $this->assertNull($this->configEntity->getUpdated()); + $this->configEntity->setUpdated(new \DateTime('2013-01-01')); + $this->assertEquals('2013-01-01', $this->configEntity->getUpdated()->format('Y-m-d')); + + /** test ConfigEntity prePersist */ + $this->configEntity->prePersist(); + $currentDate = new \DateTime('now', new \DateTimeZone('UTC')); + $this->assertEquals($currentDate->format('Y-m-d'), $this->configEntity->getCreated()->format('Y-m-d')); + + /** test ConfigEntity preUpdate */ + $this->configEntity->preUpdate(); + $currentDate = new \DateTime('now', new \DateTimeZone('UTC')); + $this->assertEquals($currentDate->format('Y-m-d'), $this->configEntity->getUpdated()->format('Y-m-d')); + + /** test ConfigField */ + $this->assertEmpty($this->configField->getId()); + + /** test ConfigValue */ + $this->assertEmpty($this->configValue->getId()); + $this->assertEmpty($this->configValue->getScope()); + $this->assertEmpty($this->configValue->getCode()); + $this->assertEmpty($this->configValue->getValue()); + $this->assertEmpty($this->configValue->getField()); + + $this->assertFalse($this->configValue->getSerializable()); + + + $this->assertEmpty($this->configValue->getEntity()); + $this->configValue->setEntity($this->configEntity); + $this->assertEquals( + $this->configEntity, + $this->configValue->getEntity() + ); + } + + public function test() + { + $this->assertEquals( + 'test', + $this->configField->getCode($this->configField->setCode('test')) + ); + + $this->assertEquals( + 'string', + $this->configField->getType($this->configField->setType('string')) + ); + + $this->assertEquals( + 'Acme\Bundle\DemoBundle\Entity\TestAccount', + $this->configEntity->getClassName( + $this->configEntity->setClassName('Acme\Bundle\DemoBundle\Entity\TestAccount') + ) + ); + + /** test ConfigField set/getEntity */ + $this->configField->setEntity($this->configEntity); + $this->assertEquals( + $this->configEntity, + $this->configField->getEntity() + ); + + /** test ConfigEntity addField */ + $this->configEntity->addField($this->configField); + $this->assertEquals( + $this->configField, + $this->configEntity->getField('test') + ); + + /** test ConfigEntity setFields */ + $this->configEntity->setFields(array($this->configField)); + $this->assertEquals( + array($this->configField), + $this->configEntity->getFields() + ); + + /** test ConfigValue */ + $this->configValue + ->setCode('is_extend') + ->setScope('extend') + ->setValue(true) + ->setField($this->configField); + + $this->assertEquals( + array( + 'code' => 'is_extend', + 'scope' => 'extend', + 'value' => true, + 'serializable' => false + ), + $this->configValue->toArray() + ); + + /** test AbstractConfig setValues() */ + $this->configEntity->setValues(array($this->configValue)); + $this->assertEquals( + $this->configValue, + $this->configEntity->getValue('is_extend', 'extend') + ); + } + + + public function testToFromArray() + { + $this->configValue + ->setCode('doctrine') + ->setScope('datagrid') + ->setValue('a:2:{s:4:"code";s:8:"test_001";s:4:"type";s:6:"string";}'); + + $values = array( + 'is_searchable' => true, + 'is_sortable' => false, + 'doctrine' => $this->configValue + ); + $serializable = array( + 'doctrine' => true + ); + + $this->configField->addValue(new ConfigValue('is_searchable', 'datagrid', false)); + $this->configField->fromArray('datagrid', $values, $serializable); + $this->assertEquals( + array( + 'is_searchable' => 1, + 'is_sortable' => 0, + 'doctrine' => $this->configValue + ), + $this->configField->toArray('datagrid') + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php new file mode 100644 index 00000000000..5ea2f1167dd --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php @@ -0,0 +1,34 @@ +configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + } + + public function testEvent() + { + $event = new NewEntityEvent(ConfigManagerTest::DEMO_ENTITY, $this->configManager); + + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getClassName()); + $this->assertEquals($this->configManager, $event->getConfigManager()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php new file mode 100644 index 00000000000..2319f365b54 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php @@ -0,0 +1,36 @@ +configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + } + + public function testEvent() + { + $event = new NewFieldEvent(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', $this->configManager); + + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getClassName()); + $this->assertEquals($this->configManager, $event->getConfigManager()); + $this->assertEquals('testField', $event->getFieldName()); + $this->assertEquals('string', $event->getFieldType()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php new file mode 100644 index 00000000000..ca3434bdb11 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php @@ -0,0 +1,134 @@ +id; + } + + /** + * Set name + * @param string $name + * @return DemoEntity + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param mixed $children + * @return $this + */ + public function setChildren($children) + { + $this->children = $children; + + return $this; + } + + /** + * @return mixed + */ + public function getChildren() + { + return $this->children; + } + + /** + * @param mixed $parent + * @return $this + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * @return mixed + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set description + * @param string $description + * @return DemoEntity + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Get description + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/NoConfigurableEntity.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/NoConfigurableEntity.php new file mode 100644 index 00000000000..349ec5ce827 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/NoConfigurableEntity.php @@ -0,0 +1,28 @@ +id; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php new file mode 100644 index 00000000000..fc351acf604 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php @@ -0,0 +1,37 @@ +addField($configField); + + $configValue = new ConfigValue('test_value', 'test', 'test_value_origin'); + $configValueSerializable = new ConfigValue('test_value_serializable', 'test', array('test_value' => 'test_value_origin')); + self::$configEntity->addValue($configValue); + self::$configEntity->addValue($configValueSerializable); + } + + return self::$configEntity; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/NotFoundEntityConfigRepository.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/NotFoundEntityConfigRepository.php new file mode 100644 index 00000000000..f889d36ce85 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/NotFoundEntityConfigRepository.php @@ -0,0 +1,13 @@ +factory = Forms::createFormFactoryBuilder() + ->addTypeExtension(new DataBlockExtension()) + ->getFormFactory(); + + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $config = Yaml::parse(file_get_contents(__DIR__ . '/../../Fixture/entity_config.yml')); + $scope = key($config['oro_entity_config']); + $config = reset($config['oro_entity_config']); + + $configProvider = new ConfigProvider($this->configManager, new EntityConfigContainer($scope, $config)); + $entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'datagrid'); + + $this->configManager->expects($this->any())->method('getConfig')->will($this->returnValue($entityConfig)); + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('getProviders')->will($this->returnValue(array($configProvider))); + } + + public function testBindValidData() + { + $formData = array( + 'datagrid' => array('enabled' => true), + 'id' => null, + ); + + $type = new ConfigEntityType($this->configManager); + $form = $this->factory->create($type, null, array( + 'class_name' => ConfigManagerTest::DEMO_ENTITY, + 'entity_id' => 1 + )); + + $form->bind($formData); + + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($formData, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php new file mode 100644 index 00000000000..2902c268bee --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php @@ -0,0 +1,94 @@ +factory = Forms::createFormFactoryBuilder() + ->addTypeExtension(new DataBlockExtension()) + ->addTypeExtension(new ConfigFormExtension()) + ->getFormFactory(); + + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $config = Yaml::parse(file_get_contents(__DIR__ . '/../../Fixture/entity_config.yml')); + $scope = key($config['oro_entity_config']); + $config = reset($config['oro_entity_config']); + + $configProvider = new ConfigProvider($this->configManager, new EntityConfigContainer($scope, $config)); + + $entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'datagrid'); + $entityConfig->addField(new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'datagrid')); + + $this->configManager->expects($this->any())->method('getConfig')->will($this->returnValue($entityConfig)); + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('getProviders')->will($this->returnValue(array($configProvider))); + } + + public function testBindValidData() + { + $formData = array( + 'id' => null, + 'datagrid' => array( + 'enabled' => true, + 'is_searchable' => true, + 'is_filtrableble' => false, + ), + ); + + $type = new ConfigFieldType($this->configManager); + + $form = $this->factory->create( + $type, + null, + array( + 'class_name' => ConfigManagerTest::DEMO_ENTITY, + 'field_name' => 'testField', + 'field_type' => 'string', + 'field_id' => 1, + ) + ); + + $form->bind($formData); + + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($formData, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php new file mode 100644 index 00000000000..d961b750e84 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php @@ -0,0 +1,21 @@ + 'hidden')); + $this->assertEquals('hidden', $annot->viewMode); + + $annot = new Configurable(array('viewMode' => 'hidden')); + $this->assertEquals('hidden', $annot->viewMode); + + $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\AnnotationException'); + + $annot = new Configurable(array('viewMode' => 'wrong_value')); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php new file mode 100644 index 00000000000..7ff9d3dc08f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php @@ -0,0 +1,34 @@ +classMetadata = new ConfigClassMetadata(ConfigManagerTest::DEMO_ENTITY); + $this->classMetadata->viewMode = 'hidden'; + } + + public function testSerialize() + { + $this->assertEquals($this->classMetadata, unserialize(serialize($this->classMetadata))); + } + + public function testMerge() + { + $newMetadata = new ConfigClassMetadata(ConfigManagerTest::DEMO_ENTITY); + $newMetadata->viewMode = 'readonly'; + $this->classMetadata->merge($newMetadata); + + $this->assertEquals('readonly', $this->classMetadata->viewMode); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php new file mode 100644 index 00000000000..5bb2aa0bc5a --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php @@ -0,0 +1,37 @@ +containerBuilder = new ContainerBuilder(); + $this->bundle = new OroEntityConfigBundle(); + } + + public function testBuild() + { + $this->bundle->build($this->containerBuilder); + + $pass = $this->containerBuilder->getCompilerPassConfig()->getBeforeOptimizationPasses(); + + $this->assertInstanceOf('Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\EntityConfigPass', $pass[0]); + $this->assertInstanceOf('Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\ServiceProxyPass', $pass[1]); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php new file mode 100644 index 00000000000..43460e0b267 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php @@ -0,0 +1,100 @@ +entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'testScope'); + $this->fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); + + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('getConfig')->will($this->returnValue($this->entityConfig)); + $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + $this->configContainer = new EntityConfigContainer('test', array()); + $this->configProvider = new ConfigProvider($this->configManager, $this->configContainer); + } + + public function testConfig() + { + $this->assertEquals(true, $this->configProvider->hasConfig(ConfigManagerTest::DEMO_ENTITY)); + $this->assertEquals($this->entityConfig, $this->configProvider->getConfig(ConfigManagerTest::DEMO_ENTITY)); + + $this->entityConfig->addField($this->fieldConfig); + + $this->assertEquals(true, $this->configProvider->hasFieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField')); + $this->assertEquals($this->fieldConfig, $this->configProvider->getFieldConfig( + ConfigManagerTest::DEMO_ENTITY, + 'testField' + )); + + $this->assertEquals('test', $this->configProvider->getScope()); + + $this->assertEquals($this->configContainer, $this->configProvider->getConfigContainer()); + } + + public function testCreateConfig() + { + $this->configProvider->createEntityConfig( + ConfigManagerTest::DEMO_ENTITY, array('first' => 'test'), true + ); + + $this->configProvider->createFieldConfig( + ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', array('first' => 'test'), true + ); + + $this->configProvider->flush(); + } + + public function testGetClassName() + { + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(ConfigManagerTest::DEMO_ENTITY)); + + $className = ConfigManagerTest::DEMO_ENTITY; + $demoEntity = new $className(); + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName($demoEntity)); + + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(array($demoEntity))); + + $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(array())); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/composer.json b/src/Oro/Bundle/EntityConfigBundle/composer.json new file mode 100644 index 00000000000..87ab3d65c2d --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/composer.json @@ -0,0 +1,29 @@ +{ + "name": "oro/entity-config-bundle", + "type": "symfony-bundle", + "description": "BAP Entity Config Bundle", + "keywords": ["BAP"], + "homepage": "https://github.com/laboro/EntityConfigBundle", + "license": "MIT", + "require": { + "php": ">=5.3.3", + "symfony/symfony": "2.3.*", + "oro/ui-bundle": "dev-master", + "oro/grid-bundle": "dev-master", + "oro/navigation-bundle": "dev-master", + "oro/data-audit-bundle": "dev-master", + "oro/form-bundle": "dev-master", + "oro/user-bundle": "dev-master", + "oro/entity-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\EntityConfigBundle": "" } + }, + "target-dir": "Oro/Bundle/EntityConfigBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/phpunit.xml.dist b/src/Oro/Bundle/EntityConfigBundle/phpunit.xml.dist new file mode 100644 index 00000000000..ed8987c8a21 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/phpunit.xml.dist @@ -0,0 +1,33 @@ + + + + + + + + ./Tests + + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php new file mode 100644 index 00000000000..d2a8869b3db --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php @@ -0,0 +1,118 @@ +setName('oro:entity-extend:backup') + ->setDescription('Backup database table(s)') + ->addArgument('entity', InputArgument::REQUIRED, 'Entity class name (REQUIRED)') + ->addArgument('path', InputArgument::OPTIONAL, 'Override configured backup path'); + } + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $parameters = $this->getContainer()->getParameterBag(); + + $this->basePath = $input->getArgument('path') ?: $parameters->get('oro_entity_extend.backup'); + $this->entity = $input->getArgument('entity'); + + if (!$this->entity) { + return; + } + + $dbms = $parameters->get('database_driver'); + $database = $parameters->get('database_name'); + $user = $parameters->get('database_user'); + $password = $parameters->get('database_password'); + $host = $parameters->get('database_host'); + + $tables = array(); + //$tables = array('oro_config_entity', 'oro_config_field'); + + /** @var ConfigProvider $extendConfigProvider */ + $extendConfigProvider = $this->getContainer()->get('oro_entity_extend.config.extend_config_provider'); + + /** @var EntityManager $em */ + $em = $this->getContainer()->get('doctrine')->getManager('default'); + + $tables[] = $em + ->getClassMetadata($extendConfigProvider->getConfig($this->entity)->get('extend_class')) + ->getTableName(); + + switch ($dbms) { + case 'pdo_mysql': + $this->database = new \Oro\Bundle\EntityExtendBundle\Databases\MySQLDatabase( + $database, + $user, + $password, + $tables, + $host + ); + break; + case 'postgresql': + //$this->database = new PostgresDatabase(); + break; + } + + $this->fileName = date('Y-m-d_H-i-s') . '.' . $this->database->getFileExtension(); + $this->filePath = $this->basePath . $this->fileName; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Start backup'); + + if (!is_dir($this->basePath)) { + mkdir($this->basePath); + } + + system($this->database->dump($this->filePath)); + + if (file_exists($this->filePath) && filesize($this->filePath) > 0) { + $output->writeln( + sprintf( + 'Database backup was successful. %s was saved in the dumps folder.', + $this->fileName + ) + ); + } else { + $output->writeln('Database backup failed'); + } + + $output->writeln('Done'); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/CheckDynamicCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/CheckDynamicCommand.php new file mode 100644 index 00000000000..fb4f5df9fa5 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Command/CheckDynamicCommand.php @@ -0,0 +1,50 @@ +setName('oro:entity-extend:check-dynamic') + ->setDescription('Check ability to regenerate schema'); + } + + /** + * Runs command + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln($this->getDescription()); + + /** @var Schema $schema */ + $schema = $this->getContainer()->get('oro_entity_extend.tools.schema'); + + $output->writeln(""); + if ($schema->checkDynamicBackend()) { + $output->writeln("Ok app can regenerate schema"); + } else { + $output->writeln("NO app cannot regenerate schema"); + } + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php new file mode 100644 index 00000000000..d6361ea7aa7 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php @@ -0,0 +1,55 @@ +setName('oro:entity-extend:generate') + ->setDescription('Generate class for doctrine'); + } + + /** + * Runs command + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln($this->getDescription()); + + /** @var EntityManager $em */ + $em = $this->getContainer()->get('doctrine.orm.default_entity_manager'); + + /** @var ExtendManager $xm */ + $xm = $this->getContainer()->get('oro_entity_extend.extend.extend_manager'); + + /** @var ConfigEntity[] $configs */ + $configs = $em->getRepository(ConfigEntity::ENTITY_NAME)->findAll(); + foreach ($configs as $config) { + if ($xm->isExtend($config->getClassName())) { + $xm->getClassGenerator()->checkEntityCache($config->getClassName(), true); + }; + } + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php new file mode 100644 index 00000000000..fa7c297f70e --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php @@ -0,0 +1,162 @@ +getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + /** @var ConfigProvider $extendConfigProvider */ + $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + + /** @var Schema $schemaTools */ + $schemaTools = $this->get('oro_entity_extend.tools.schema'); + + /** + * do Validations + */ + $validation = array(); + + $fields = $extendConfig->getFields(); + foreach ($fields as $code => $field) { + + //$isSystem = $schemaTools->checkFieldIsSystem($field); + $isSystem = $field->get('owner') == 'System' ? true : false; + if ($isSystem) { + continue; + } + + if (in_array($field->get('state'), array('New', 'Updated', 'To be deleted'))) { + if ($field->get('state') == 'New') { + $isValid = true; + } else { + $isValid = $schemaTools->checkFieldCanDelete($field); + } + + if ($isValid) { + $validation['success'][] = sprintf( + "Field '%s(%s)' is valid. State -> %s", + $code, + $field->get('owner'), + $field->get('state') + ); + } else { + $validation['error'][] = sprintf( + "Warning. Field '%s(%s)' has data.", + $code, + $field->get('owner') + ); + } + } + } + + return array( + 'validations' => $validation, + 'entity' => $entity, + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + 'entity_extend' => $extendConfig, + ); + } + + /** + * @Route( + * "/update/{id}", + * name="oro_entityextend_update", + * requirements={"id"="\d+"}, + * defaults={"id"=0} + * ) + * @Acl( + * id="oro_entityextend_update", + * name="Apply changes", + * description="Apply entityconfig changes", + * parent="oro_entityextend" + * ) + * @Template() + */ + public function updateAction($id) + { + /** @var ConfigEntity $entity */ + $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $env = $this->get('kernel')->getEnvironment(); + + $commands = array( + 'backup' => new Process('php ../app/console oro:entity-extend:backup '. str_replace('\\', '\\\\', $entity->getClassName()). ' --env '.$env), + 'generator' => new Process('php ../app/console oro:entity-extend:generate'. ' --env '.$env), + 'cacheClear' => new Process('php ../app/console cache:clear --no-warmup'. ' --env '.$env), + 'schemaUpdate' => new Process('php ../app/console doctrine:schema:update --force'. ' --env '.$env), + 'searchIndex' => new Process('php ../app/console oro:search:create-index --env '.$env), + 'cacheWarmup' => new Process('php ../app/console cache:warmup'. ' --env '.$env), + ); + + foreach ($commands as $command) { + /** @var $command Process */ + $command->run(); + + while ($command->isRunning()) { + /** wait for previous process */ + } + } + + /** @var ConfigProvider $extendConfigProvider */ + $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + + $extendConfig->set('state', 'Active'); + + foreach ($extendConfig->getFields() as $field) { + if ($field->get('owner') != 'System') { + $field->set('state', 'Active'); + } + } + + $extendConfigProvider->persist($extendConfig); + $extendConfigProvider->flush(); + + return $this->redirect($this->generateUrl('oro_entityconfig_view', array('id' => $id))); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php new file mode 100644 index 00000000000..3a956c4b018 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php @@ -0,0 +1,117 @@ +get('oro_entity_extend.config.extend_config_provider'); + $entityConfig = $configProvider->getConfig($entity->getClassName()); + + $data = $entityConfig->has('unique_key') ? $entityConfig->get('unique_key') : array(); + + $request = $this->getRequest(); + + $form = $this->createForm( + new UniqueKeyCollectionType( + $entityConfig->getFields( + function (FieldConfig $fieldConfig) { + return $fieldConfig->getType() != 'ref-many'; + } + ) + ), + $data + ); + + if ($request->getMethod() == 'POST') { + $form->bind($request); + + if ($form->isValid()) { + $data = $form->getData(); + + $error = false; + $names = array(); + foreach ($data['keys'] as $key) { + if (in_array($key['name'], $names)) { + $error = true; + $this->get('session')->getFlashBag()->add( + 'error', + sprintf('Name for key should be unique, key "%s" is not unique.', $key['name']) + ); + break; + } + + if (empty($key['name'])) { + $error = true; + $this->get('session')->getFlashBag()->add( + 'error', + 'Name of key can\'t be empty.' + ); + break; + } + + $names[] = $key['name']; + } + + if (!$error) { + $entityConfig->set('unique_key', $data); + $configProvider->persist($entityConfig); + $configProvider->flush(); + + return $this->redirect( + $this->generateUrl('oro_entityconfig_view', array('id' => $entity->getId())) + ); + } + } + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + return array( + 'form' => $form->createView(), + 'entity_id' => $entity->getId(), + 'entity_config' => $entityConfigProvider->getConfig($entityConfig->getClassName()) + ); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php new file mode 100644 index 00000000000..45e28488c7d --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php @@ -0,0 +1,141 @@ +get('oro_entity_extend.extend.extend_manager'); + + if (!$extendManager->isExtend($entity->getClassName())) { + $this->get('session')->getFlashBag()->add('error', $entity->getClassName() . 'isn\'t extend'); + + return $this->redirect($this->generateUrl('oro_entityconfig_fields', + array( + 'id' => $entity->getId() + ) + )); + } + + $request = $this->getRequest(); + $form = $this->createForm(new FieldType()); + + if ($request->getMethod() == 'POST') { + $form->submit($request); + + if ($form->isValid()) { + $data = $form->getData(); + + if ($entity->getField($data['code'])) { + $form->get('code')->addError(new FormError(sprintf( + "Field '%s' already exist in entity '%s', ", $data['code'], $entity->getClassName() + ))); + } else { + $extendManager->getConfigFactory()->createFieldConfig($entity->getClassName(), $data); + + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + $configManager->clearCache($entity->getClassName()); + + $this->get('session')->getFlashBag()->add('success', sprintf( + 'field "%s" has been added to entity "%', $data['code'], $entity->getClassName() + )); + + return $this->redirect($this->generateUrl('oro_entityconfig_field_update', + array( + 'id' => $entity->getField($data['code'])->getId() + ) + )); + } + } + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + + return array( + 'form' => $form->createView(), + 'entity_id' => $entity->getId(), + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + ); + } + + /** + * @Route( + * "/remove/{id}", + * name="oro_entityextend_field_remove", + * requirements={"id"="\d+"}, + * defaults={"id"=0} + * ) + * @Acl( + * id="oro_entityextend_field_remove", + * name="Remove custom field", + * description="Update entity remove custom field", + * parent="oro_entityextend" + * ) + */ + public function removeAction(ConfigField $field) + { + if (!$field) { + throw $this->createNotFoundException('Unable to find ConfigField entity.'); + } + + /** @var ExtendManager $extendManager */ + $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); + + $fieldConfig = $extendManager->getConfigProvider() + ->getFieldConfig($field->getEntity()->getClassName(), $field->getCode()); + if (!$fieldConfig->is('is_extend')) { + return new Response('', Codes::HTTP_FORBIDDEN); + } + + $this->getDoctrine()->getManager()->remove($field); + $this->getDoctrine()->getManager()->flush($field); + + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + $configManager->clearCache($fieldConfig->getClassName()); + + return new Response('', Codes::HTTP_NO_CONTENT); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Databases/DatabaseInterface.php b/src/Oro/Bundle/EntityExtendBundle/Databases/DatabaseInterface.php new file mode 100644 index 00000000000..19707b3d64e --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Databases/DatabaseInterface.php @@ -0,0 +1,29 @@ +database = $database; + $this->user = $user; + $this->password = $password; + $this->host = $host; + $this->tables = '"' . implode('" "', $tables) . '"'; + } + + /** + * {@inheritdoc} + */ + public function dump($destinationFile) + { + $command = sprintf( + 'mysqldump --user="%s" --password="%s" --host="%s" "%s" %s > "%s"', + $this->user, + $this->password, + $this->host, + $this->database, + $this->tables, + $destinationFile + ); + + return $command; + } + + /** + * {@inheritdoc} + */ + public function restore($sourceFile) + { + $command = sprintf( + 'mysql --user="%s" --password="%s" --host="%s" "%s" < "%s"', + $this->user, + $this->password, + $this->host, + $this->database, + $sourceFile + ); + + return $command; + } + + /** + * {@inheritdoc} + */ + public function getFileExtension() + { + return 'sql'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Databases/PostgresDatabase.php b/src/Oro/Bundle/EntityExtendBundle/Databases/PostgresDatabase.php new file mode 100644 index 00000000000..b2d1beac56d --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Databases/PostgresDatabase.php @@ -0,0 +1,70 @@ +database = $database; + $this->user = $user; + $this->password = $password; + $this->host = $host; + $this->tables = '"' . implode('" "', $tables) . '"'; + } + + /** + * {@inheritdoc} + */ + public function dump($destinationFile) + { + $command = sprintf( + 'PGPASSWORD="%s" pg_dump -Fc --no-acl --no-owner -h "%s" -U "%s" "%s" > "%s"', + $this->password, + $this->host, + $this->user, + $this->database, + $destinationFile + ); + + return $command; + } + + /** + * {@inheritdoc} + */ + public function restore($sourceFile) + { + $command = sprintf( + 'PGPASSWORD="%s" pg_restore --verbose --clean --no-acl --no-owner -h "%s" -U "%s" -d "%s" "%s"', + $this->password, + $this->host, + $this->user, + $this->database, + $sourceFile + ); + + return $command; + } + + /** + * {@inheritdoc} + */ + public function getFileExtension() + { + return 'dump'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..f6ca7ad5348 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/Configuration.php @@ -0,0 +1,39 @@ +root('oro_entity_extend') + ->children() + ->scalarNode('backend') + ->isRequired() + ->validate() + ->ifNotInArray(array('Dynamic', 'EAV')) + ->thenInvalid('Invalid mode value "%s"') + ->end() + ->end() + ->scalarNode('cache_dir')->cannotBeEmpty()->defaultValue('%kernel.cache_dir%/oro_extend')->end() + #->scalarNode('extend_namespace')->cannotBeEmpty()->defaultValue('Extend\Entity')->end() + ->end() + ->children() + ->scalarNode('backup')->cannotBeEmpty()->defaultValue('%kernel.root_dir%/entities/Backup')->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php new file mode 100644 index 00000000000..9a916e13e9a --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php @@ -0,0 +1,75 @@ +processConfiguration($configuration, $configs); + + $this->configBackend($container, $config); + $this->configCache($container, $config); + + $fileLocator = new FileLocator(__DIR__ . '/../Resources/config'); + $loader = new Loader\YamlFileLoader($container, $fileLocator); + $loader->load('services.yml'); + $loader->load('metadata.yml'); + } + + protected function configBackend(ContainerBuilder $container, $config) + { + $backend = $container->getParameterBag()->resolveValue($config['backend']); + $path = $container->getParameterBag()->resolveValue($config['backup']); + + $container->setParameter('oro_entity_extend.backend', $backend); + $container->setParameter('oro_entity_extend.backup', $path); + + // for DoctrineOrmMappingsPass end BackendCompilerPass. Detect with backend should be mapped and loaded + $container->setParameter('oro_entity_extend.backend.' . strtolower($backend), true); + } + + /** + * @param ContainerBuilder $container + * @param $config + * @throws RuntimeException + */ + protected function configCache(ContainerBuilder $container, $config) + { + $cacheDir = $container->getParameterBag()->resolveValue($config['cache_dir']); + + $fs = new Filesystem(); + $fs->remove($cacheDir); + + if (!is_dir($cacheDir)) { + if (false === @mkdir($cacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); + } + } + $container->setParameter('oro_entity_extend.cache_dir', $cacheDir); + + $annotationCacheDir = $cacheDir . '/annotation'; + if (!is_dir($annotationCacheDir)) { + if (false === @mkdir($annotationCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create annotation cache directory "%s".', $annotationCacheDir)); + } + } + $container->setParameter('oro_entity_extend.cache_dir.annotation', $annotationCacheDir); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Entity/ExtendEntityInterface.php b/src/Oro/Bundle/EntityExtendBundle/Entity/ExtendEntityInterface.php new file mode 100644 index 00000000000..982ee92fa69 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Entity/ExtendEntityInterface.php @@ -0,0 +1,14 @@ +extendManager = $extendManager; + $this->metadataFactory = $metadataFactory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + Events::NEW_ENTITY => 'newEntityConfig', + Events::PERSIST_CONFIG => 'persistConfig', + ); + } + + /** + * @param NewEntityEvent $event + */ + public function newEntityConfig(NewEntityEvent $event) + { + /** @var ExtendClassMetadata $metadata */ + $metadata = $this->metadataFactory->getMetadataForClass($event->getClassName()); + if ($metadata && $metadata->isExtend) { + $extendClass = $this->extendManager->getClassGenerator()->generateExtendClassName($event->getClassName()); + $proxyClass = $this->extendManager->getClassGenerator()->generateProxyClassName($event->getClassName()); + + $this->extendManager->getConfigProvider()->createEntityConfig( + $event->getClassName(), + $values = array( + 'is_extend' => true, + 'extend_class' => $extendClass, + 'proxy_class' => $proxyClass, + 'owner' => 'System', + ) + ); + } + } + + /** + * @param PersistConfigEvent $event + */ + public function persistConfig(PersistConfigEvent $event) + { + $event->getConfigManager()->calculateConfigChangeSet($event->getConfig()); + $change = $event->getConfigManager()->getConfigChangeSet($event->getConfig()); + + if ($event->getConfig()->getScope() == 'extend' + && $event->getConfig()->is('is_extend') + && count(array_intersect_key(array_flip(array('length', 'precision', 'scale')), $change)) + && $event->getConfig()->get('state') != ExtendManager::STATE_NEW + ) { + $entityConfig = $event->getConfigManager()->getProvider($event->getConfig()->getScope())->getConfig($event->getConfig()->getClassName()); + $event->getConfig()->set('state', ExtendManager::STATE_UPDATED); + $entityConfig->set('state', ExtendManager::STATE_UPDATED); + + $event->getConfigManager()->persist($entityConfig); + } + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/EventListener/DoctrineSubscriber.php b/src/Oro/Bundle/EntityExtendBundle/EventListener/DoctrineSubscriber.php new file mode 100644 index 00000000000..4cc4b83e8bf --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/EventListener/DoctrineSubscriber.php @@ -0,0 +1,98 @@ +exm = $extendManager; + } + + /** + * {@inheritdoc} + */ + public function getSubscribedEvents() + { + return array( + 'preRemove', + 'preUpdate', + 'prePersist', + 'postLoad', + 'loadClassMetadata' + ); + } + + /** + * @param LifecycleEventArgs $event + */ + public function preRemove(LifecycleEventArgs $event) + { + /*if ($event->getEntity() instanceof ExtendProxyInterface) { + $event->getEntityManager()->remove($event->getEntity()->__proxy__getExtend()); + }*/ + } + + /** + * @param LifecycleEventArgs $event + */ + public function preUpdate(LifecycleEventArgs $event) + { + if ($event->getEntity() instanceof ExtendProxyInterface) { + $event->getEntityManager()->remove($event->getEntity()->__proxy__getExtend()); + } + } + + /** + * @param LifecycleEventArgs $event + */ + public function prePersist(LifecycleEventArgs $event) + { + /*if ($event->getEntity() instanceof ExtendProxyInterface + || $this->exm->isExtend($event->getEntity()) + ) { + $this->exm->persist($event->getEntity()); + }*/ + } + + /** + * @param LifecycleEventArgs $event + */ + public function postLoad(LifecycleEventArgs $event) + { + /*if ($event->getEntity() instanceof ExtendProxyInterface + || $this->exm->isExtend($event->getEntity()) + ) { + $this->exm->loadExtend($event->getEntity()); + }*/ + } + + /** + * @param LoadClassMetadataEventArgs $event + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $event) + { + /*if ($this->exm->isExtend($event->getClassMetadata()->name)) { + $proxyRef = new \ReflectionClass($this->exm->getProxyClass($event->getClassMetadata()->name)); + + $event->getClassMetadata()->name = $proxyRef->getName(); + $event->getClassMetadata()->namespace = $proxyRef->getNamespaceName(); + }*/ + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Exception/RuntimeException.php b/src/Oro/Bundle/EntityExtendBundle/Exception/RuntimeException.php new file mode 100644 index 00000000000..3fd6c69bbd5 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Exception/RuntimeException.php @@ -0,0 +1,7 @@ +lazyEm = $lazyEm; + $this->configProvider = $configProvider; + $this->proxyFactory = new ProxyObjectFactory($this); + $this->configFactory = new ConfigFactory($this); + $this->generator = new Generator($configProvider, $backend, $entityCacheDir); + } + + /** + * @return ConfigProvider + */ + public function getConfigProvider() + { + return $this->configProvider; + } + + /** + * @return \Doctrine\ORM\EntityManager + */ + public function getEntityManager() + { + return $this->lazyEm->getService(); + } + + /** + * @return ProxyObjectFactory + */ + public function getProxyFactory() + { + return $this->proxyFactory; + } + + /** + * @return ConfigFactory + */ + public function getConfigFactory() + { + return $this->configFactory; + } + + /** + * @return Generator + */ + public function getClassGenerator() + { + return $this->generator; + } + + /** + * @param $entityName + * @return bool|string + */ + public function isExtend($entityName) + { + if ($entityName + && $this->configProvider->hasConfig($entityName) + && $this->configProvider->getConfig($entityName)->is('is_extend') + ) { + return true; + } + + return false; + } + + /** + * @param $entityName + * @return null|string + */ + public function getExtendClass($entityName) + { + return $this->configProvider->getConfig($entityName)->get('extend_class'); + } + + /** + * @param $entityName + * @return null|string + */ + public function getProxyClass($entityName) + { + return $this->configProvider->getConfig($entityName)->get('proxy_class'); + } + + /** + * @param $entity + */ + public function loadExtend($entity) + { + $proxy = $this->getProxyFactory()->getProxyObject($entity); + $this->getProxyFactory()->initExtendObject($proxy); + } + + public function persist($entity) + { + if ($this->isExtend($entity)) { + $proxy = $this->getProxyFactory()->getProxyObject($entity); + $proxy->__proxy__createFromEntity($entity); + + $this->getEntityManager()->detach($entity); + $this->getEntityManager()->persist($proxy); + $this->getEntityManager()->persist($proxy->__proxy__getExtend()); + } + + if ($entity instanceof ExtendProxyInterface) { + $this->getEntityManager()->persist($entity->__proxy__getExtend()); + } + } + + /** + * @param $entity + */ + public function remove($entity) + { + $extend = $this->getProxyFactory()->getProxyObject($entity); + $this->getEntityManager()->remove($extend); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendRepository.php b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendRepository.php new file mode 100644 index 00000000000..a2e2146f41b --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendRepository.php @@ -0,0 +1,60 @@ +config = $config; + } + + /** + * {@inheritdoc} + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * {@inheritdoc} + */ + public function findAll() + { + // TODO: Implement findAll() method. + } + + /** + * {@inheritdoc} + */ + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + { + // TODO: Implement findBy() method. + } + + /** + * {@inheritdoc} + */ + public function findOneBy(array $criteria) + { + // TODO: Implement findOneBy() method. + } + + /** + * {@inheritdoc} + */ + public function getClassName() + { + return $this->config->getClassName(); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php b/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php new file mode 100644 index 00000000000..ca6fab522ce --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php @@ -0,0 +1,62 @@ +extendManager = $extendManager; + } + + public function createFieldConfig($className, $data) + { + $values = array(); + + $values['is_extend'] = true; + $values['owner'] = ExtendManager::OWNER_CUSTOM; + $values['state'] = ExtendManager::STATE_NEW; + + $constraint = array( + 'property' => array(), + 'constraint' => array() + ); + + if ($data['type'] == 'string') { + $constraint['property']['Symfony\Component\Validator\Constraints\Length'] = array('max' => 255); + } + + + if ($data['type'] == 'datetime') { + $constraint['property']['Symfony\Component\Validator\Constraints\DateTime'] = array(); + } + + if ($data['type'] == 'date') { + $constraint['property']['Symfony\Component\Validator\Constraints\Date'] = array(); + } + + $values['constraint'] = serialize($constraint); + + $entityConfig = $this->extendManager->getConfigProvider()->getConfig($className); + $entityConfig->set('state', ExtendManager::STATE_UPDATED); + $this->extendManager->getConfigProvider()->persist($entityConfig); + + $this->extendManager->getConfigProvider()->createFieldConfig( + $className, + $data['code'], + $data['type'], + $values, + true + ); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/ProxyObjectFactory.php b/src/Oro/Bundle/EntityExtendBundle/Extend/ProxyObjectFactory.php new file mode 100644 index 00000000000..7a04ebcb080 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Extend/ProxyObjectFactory.php @@ -0,0 +1,103 @@ +extendManager = $extendManager; + } + + /** + * @param $entity + * @return ExtendProxyInterface + */ + public function getProxyObject($entity) + { + if (isset($this->proxyObjects[spl_object_hash($entity)])) { + return $this->proxyObjects[spl_object_hash($entity)]; + } else { + return $this->initProxyObject($entity); + } + } + + /** + * @param $entity + * @return bool + */ + public function hasProxyObject($entity) + { + return isset($this->proxyObjects[spl_object_hash($entity)]); + } + + /** + * @param $entity + */ + public function removeProxyObject($entity) + { + if (isset($this->proxyObjects[spl_object_hash($entity)])) { + unset($this->proxyObjects[spl_object_hash($entity)]); + } + } + + /** + * @param $entity + * @return null|ExtendEntityInterface + */ + public function initExtendObject(ExtendProxyInterface $entity) + { + $entityClass = get_parent_class($entity); + $extendClass = $this->extendManager->getExtendClass($entityClass); + + $em = $this->extendManager->getEntityManager(); + $extend = $em->getUnitOfWork()->isEntityScheduled($entity) + ? $em + ->getRepository($extendClass) + ->findOneBy(array('__extend__parent' => $em->getUnitOfWork()->getEntityIdentifier($entity))) + : null; + + if (!$extend) { + /** @var ExtendEntityInterface $extend */ + $extend = new $extendClass(); + $extend->__extend__setParent($entity); + } + + $entity->__proxy__setExtend($extend); + + return $this->proxyObjects[spl_object_hash($entity)] = $extend; + } + + /** + * @param $entity + * @return null|\Oro\Bundle\EntityExtendBundle\Entity\ExtendProxyInterface + */ + protected function initProxyObject($entity) + { + if (!$entity instanceof ExtendProxyInterface) { + $proxyClass = $this->extendManager->getProxyClass($entity); + $proxy = new $proxyClass(); + $proxy->__proxy__createFromEntity($entity); + $entity = $proxy; + $this->extendManager->getProxyFactory()->getProxyObject($entity); + } + + return $entity; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Extension/ExtendEntityExtension.php b/src/Oro/Bundle/EntityExtendBundle/Form/Extension/ExtendEntityExtension.php new file mode 100644 index 00000000000..84cd1f60cf6 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Extension/ExtendEntityExtension.php @@ -0,0 +1,62 @@ +extendManager = $extendManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $xm = $this->extendManager; + + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($xm) { + $data = $event->getData(); + //TODO::check empty data end data_class + if (is_object($data) && $xm->isExtend($data)) { + $event->setData($xm->createProxyObject($data)); + } + }); + + $builder->addEventListener(FormEvents::POST_BIND, function (FormEvent $event) use ($xm) { + $data = $event->getForm()->getConfig()->getData(); + + if (is_object($data) && $xm->isExtend($data)) { + if ($event->getData() instanceof ExtendProxyInterface) { + $event->getData()->__proxy__cloneToEntity($data); + } + } + }); + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php new file mode 100644 index 00000000000..1727cda86fb --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php @@ -0,0 +1,58 @@ +add('code', 'text', array( + 'label' => 'Field Name', + 'block' => 'type', + )); + $builder->add('type', 'choice', array( + 'choices' => array_combine(array_values($this->types), $this->types), + 'empty_value' => 'Please choice type...', + 'block' => 'type', + )); + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'block_config' => array( + 'type' => array( + 'title' => 'Doctrine Type', + 'priority' => 1, + ) + ) + )); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_extend_field_type'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php new file mode 100644 index 00000000000..44d8f2253d6 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php @@ -0,0 +1,50 @@ +fields = $fields; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add( + 'keys', + 'collection', + array( + 'required' => true, + 'type' => new UniqueKeyType($this->fields), + 'allow_add' => true, + 'allow_delete' => true, + 'prototype' => true, + 'prototype_name' => 'tag__name__', + 'label' => ' ' + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_extend_unique_key_collection_type'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php new file mode 100644 index 00000000000..edc7416d13e --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php @@ -0,0 +1,56 @@ +fields = $fields; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $choices = $this->fields->map(function (FieldConfig $field) { + return ucfirst($field->getCode()); + }); + + $builder->add( + 'name', + 'text', + array( + 'required' => true, + ) + ); + + $builder->add( + 'key', + 'choice', + array( + 'multiple' => true, + 'choices' => $choices->toArray(), + 'required' => true, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_extend_unique_key_type'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Mapping/ExtendClassMetadataFactory.php b/src/Oro/Bundle/EntityExtendBundle/Mapping/ExtendClassMetadataFactory.php new file mode 100644 index 00000000000..ce17feac133 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Mapping/ExtendClassMetadataFactory.php @@ -0,0 +1,17 @@ +reader = $reader; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass(\ReflectionClass $class) + { + $metadata = new ExtendClassMetadata($class->getName()); + + if ($this->reader->getClassAnnotation($class, self::EXTEND)) { + $metadata->isExtend = true; + } + + return $metadata; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php b/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php new file mode 100644 index 00000000000..c4a66287e9f --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php @@ -0,0 +1,50 @@ +isExtend = $object->isExtend; + } + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->isExtend, + parent::serialize(), + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($str) + { + list( + $this->isExtend, + $parentStr + ) = unserialize($str); + + parent::unserialize($parentStr); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php b/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php new file mode 100644 index 00000000000..520d7524220 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php @@ -0,0 +1,45 @@ +getParameter('kernel.root_dir') . '/entities/Extend/Entity'; + if (!is_dir($entityCacheDir)) { + if (false === @mkdir($entityCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create entity cache directory "%s".', $entityCacheDir)); + } + } + $proxyCacheDir = $container->getParameter('kernel.root_dir') . '/entities/Extend/Proxy'; + if (!is_dir($proxyCacheDir)) { + if (false === @mkdir($proxyCacheDir, 0777, true)) { + throw new RuntimeException(sprintf('Could not create proxy cache directory "%s".', $proxyCacheDir)); + } + } + + $container->addCompilerPass( + DoctrineOrmMappingsPass::createYamlMappingDriver( + array($entityCacheDir . '/EAV' => 'Extend\Entity\EAV',), + array(), + 'oro_entity_extend.backend.eav' + ) + ); + + $container->addCompilerPass( + DoctrineOrmMappingsPass::createYamlMappingDriver( + array($entityCacheDir . '/Dynamic' => 'Extend\Entity\Dynamic',), + array(), + 'oro_entity_extend.backend.dynamic' + ) + ); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/README.md b/src/Oro/Bundle/EntityExtendBundle/README.md new file mode 100644 index 00000000000..5ffadb8c062 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/README.md @@ -0,0 +1,2 @@ +OroEntityExtendBundle +======================== diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml new file mode 100644 index 00000000000..fce1b9b9717 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml @@ -0,0 +1,7 @@ +js: + 'entityextend': + - '@OroEntityExtendBundle/Resources/public/js/extend.apply.js' + - '@OroEntityExtendBundle/Resources/public/js/extend.field.create.js' +css: + 'entityextend': + - '@OroEntityExtendBundle/Resources/public/css/extend.css' diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml new file mode 100644 index 00000000000..7f821b4f981 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml @@ -0,0 +1,165 @@ +oro_entity_config: + extend: + entity: + items: + owner: + options: + priority: 40 + internal: true + default_value: 'System' + grid: + type: string + label: 'Type' + filter_type: oro_grid_orm_string + required: true + sortable: true + filterable: true + show_filter: true + form: + type: text + block: entity + options: + read_only: true + required: false + label: 'Type' + view: + type: string + stats: false + block: + is_extend: + options: + default_value: false + internal: true + state: + options: + priority: 25 + default_value: 'Active' + label: 'Status' + grid: + type: string + label: 'Status' + filter_type: oro_grid_orm_choice + choices: {system: '', new: 'New', active: 'Active', updated: 'Updated', deleted: 'To be deleted'} + required: true + sortable: true + filterable: false + show_filter: false + extend_class: + options: + internal: true + unique_key: + options: + internal: true + serializable: true + proxy_class: + options: + internal: true + + field: + grid_action: + - {'name':'Remove', 'route':'oro_entityextend_field_remove', 'type':'delete', 'icon':'trash', 'acl_resource':'root', 'filter':'is_extend', 'args':['id']} + layout_action: + - {'name':'Manage unique keys', 'route':'oro_entityextend_entity_unique_key', 'entity_id':true} + - {'name':'Create field', 'route':'oro_entityextend_field_create', 'entity_id':true, 'filter':'is_extend'} + - {'name':'Update schema', 'route':'oro_entityextend_update', 'entity_id':true, 'aClass':'btn-danger entity-extend-apply', 'void':true} + items: + owner: + options: + priority: 20 + default_value: 'System' + internal: true + grid: + type: string + label: 'Type' + filter_type: oro_grid_orm_string + required: true + sortable: true + filterable: false + show_filter: false + + constraint: + options: + internal: true + + state: + options: + priority: 25 + default_value: 'Active' + label: 'Status' + grid: + type: string + label: 'Status' + filter_type: oro_grid_orm_choice + choices: { new: 'New', applied: 'Applied', updated: 'Updated', deleted: 'To be deleted'} + required: true + sortable: true + filterable: false + show_filter: false + + is_extend: + options: + priority: 40 + default_value: false + is_bool: true + internal: true +# grid: +# type: boolean +# label: 'Is Extend Field' +# filter_type: oro_grid_orm_boolean +# required: true +# sortable: true +# filterable: false +# show_filter: false +# form: +# type: choice +# block: entity +# options: +# choices: ['No', 'Yes'] +# read_only: true +# required: false +# label: "Is Extend Field" + + length: + options: + default_value: 255 + required_property: + property_path: extend.owner + value: Custom + form: + type: text + options: + required: false + label: 'Length' + allowed_type: 'string' + block: entity + subblock: properties + + precision: + options: + default_value: 2 + required_property: + property_path: extend.owner + value: Custom + form: + type: text + options: + required: false + label: 'Precision' + allowed_type: 'decimal' + block: entity + subblock: properties + + scale: + options: + default_value: 2 + required_property: + property_path: extend.owner + value: Custom + form: + type: text + options: + required: false + label: 'Scale' + allowed_type: 'decimal' + block: entity + subblock: properties diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml new file mode 100644 index 00000000000..f03d1322495 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml @@ -0,0 +1,22 @@ +parameters: + oro_entity_extend.metadata.metadata_factory.class: Metadata\MetadataFactory + oro_entity_extend.metadata.annotation_driver.class: Oro\Bundle\EntityExtendBundle\Metadata\Driver\AnnotationDriver + oro_entity_extend.metadata.cache.file_cache.class: Metadata\Cache\FileCache + +services: + oro_entity_extend.metadata.annotation_metadata_factory: + class: %oro_entity_extend.metadata.metadata_factory.class% + arguments: [@oro_entity_extend.metadata.annotation_driver] + public: false + calls: + - [setCache, [@oro_entity_extend.metadata.cache.file_cache.annotation]] + + oro_entity_extend.metadata.annotation_driver: + class: %oro_entity_extend.metadata.annotation_driver.class% + arguments: [@annotation_reader] + public: false + + oro_entity_extend.metadata.cache.file_cache.annotation: + class: %oro_entity_extend.metadata.cache.file_cache.class% + arguments: [%oro_entity_extend.cache_dir.annotation%] + public: false diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/routing.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/routing.yml new file mode 100644 index 00000000000..e6c129d64c9 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/routing.yml @@ -0,0 +1,4 @@ +oro_entity_config_bundle: + resource: "@OroEntityExtendBundle/Controller" + type: annotation + prefix: / diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml new file mode 100644 index 00000000000..826dfc8a341 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml @@ -0,0 +1,50 @@ +parameters: + oro_entity_extend.extend.extend_manager.class: Oro\Bundle\EntityExtendBundle\Extend\ExtendManager + oro_entity_extend.listener.doctrine_subscriber.class: Oro\Bundle\EntityExtendBundle\EventListener\DoctrineSubscriber + oro_entity_extend.listener.config_subscriber.class: Oro\Bundle\EntityExtendBundle\EventListener\ConfigSubscriber + oro_entity_extend.tools.schema.class: Oro\Bundle\EntityExtendBundle\Tools\Schema + oro_entity_extend.extension.extend_entity.class: Oro\Bundle\EntityExtendBundle\Form\Extension\ExtendEntityExtension + + oro_entity_extend.command.backup.class: Oro\Bundle\EntityExtendBundle\Command\BackupCommand + oro_entity_extend.command.generate.class: Oro\Bundle\EntityExtendBundle\Command\GenerateCommand + +services: + oro_entity_extend.extend.extend_manager: + class: %oro_entity_extend.extend.extend_manager.class% + arguments: [@oro_entity_config.proxy.entity_manager, @oro_entity_extend.config.extend_config_provider, %oro_entity_extend.backend%, %kernel.root_dir%/entities] + + oro_entity_extend.config.extend_config_provider: + tags: + - { name: oro_entity_config.provider, scope: extend } + + oro_entity_extend.tools.schema: + class: %oro_entity_extend.tools.schema.class% + arguments: [@doctrine.orm.default_entity_manager, %oro_entity_extend.backend%, @oro_entity_extend.extend.extend_manager] + + oro_entity_extend.listener.doctrine_subscriber: + class: %oro_entity_extend.listener.doctrine_subscriber.class% + arguments: [@oro_entity_extend.extend.extend_manager] + tags: + - { name: doctrine.event_subscriber, connection: default } + + oro_entity_extend.listener.config_subscriber: + class: %oro_entity_extend.listener.config_subscriber.class% + arguments: [@oro_entity_extend.extend.extend_manager, @oro_entity_extend.metadata.annotation_metadata_factory] + tags: + - { name: kernel.event_subscriber} + + oro_entity_extend.command.backup: + class: %oro_entity_extend.command.backup.class% + calls: + - [setContainer, ["@service_container"]] + + oro_entity_extend.command.generate: + class: %oro_entity_extend.command.generate.class% + calls: + - [setContainer, ["@service_container"]] + +# oro_entity_extend.extension.extend_entity: +# class: %oro_entity_extend.extension.extend_entity.class% +# arguments: [@oro_entity_extend.extend.extend_manager] +# tags: +# - { name: form.type_extension, alias: form } diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/public/css/extend.css b/src/Oro/Bundle/EntityExtendBundle/Resources/public/css/extend.css new file mode 100644 index 00000000000..c2675dbfd7d --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/public/css/extend.css @@ -0,0 +1,3 @@ +#entity_extend_unique_key_collection select{ + margin:0; +} \ No newline at end of file diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.apply.js b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.apply.js new file mode 100644 index 00000000000..a35b5e06bde --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.apply.js @@ -0,0 +1,39 @@ +$(function() { + $(document).on('click', '.entity-extend-apply', function (e) { + var el = $(this); + var message = el.data('message'); + var doAction = function() { + confirmUpdate.preventClose(function(){}); + + var url = $(el).attr('href').substr(21); + var progressbar = $('#progressbar').clone(); + progressbar + .attr('id', 'confirmUpdateLoading') + .css({'display':'block', 'margin': '0 auto'}) + .find('h3').remove(); + + confirmUpdate.$content.parent().find('a.cancel').hide(); + confirmUpdate.$content.parent().find('a.close').hide(); + confirmUpdate.$content.parent().find('a.btn-danger').replaceWith(progressbar); + + $('#confirmUpdateLoading').show(); + window.location.href = url; + }; + + if (!_.isUndefined(Oro.BootstrapModal)) { + var confirmUpdate = new Oro.BootstrapModal({ + allowCancel: true, + cancelText: _.__('Cancel'), + title: 'Schema update confirmation', + content: '

    '+ _.__('Your config changes will be applied to schema.')+'

    '+ _.__('It may take few minutes...')+'

    ', + okText: 'Yes, Proceed' + }); + confirmUpdate.on('ok', doAction); + confirmUpdate.open(); + } else if (window.confirm(message)) { + doAction(); + } + + return false; + }); +}); diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js new file mode 100644 index 00000000000..811a84594a1 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js @@ -0,0 +1,17 @@ +$(function() { + var select = 'form#oro_entity_extend_field_type select#oro_entity_extend_field_type_type'; + $(select).change(function() { + var selected = $(select + ' option:selected').attr('value'); + $('div#oro_entity_extend_field_type_options_extend input[data-allowedtype]').each(function(index, el) { + if ($(el).data('allowedtype').indexOf(selected) != -1) { + + $(el).removeClass('hide'); + $(el).parents('.control-group:first').removeClass('hide'); + } + else { + $(el).addClass('hide'); + $(el).parents('.control-group:first').addClass('hide'); + } + }) + }) +}); diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/translations/messages.en.yml new file mode 100644 index 00000000000..eb7d6da0504 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/translations/messages.en.yml @@ -0,0 +1,3 @@ +"Your config changes will be applied to schema.": "Your config changes will be applied to schema." +"It may take few minutes...": "It may take few minutes..." +"Cancel": "Cancel" diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/Apply/apply.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/Apply/apply.html.twig new file mode 100644 index 00000000000..ffa936581ed --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/Apply/apply.html.twig @@ -0,0 +1,36 @@ +{% import 'OroUIBundle::macros.html.twig' as UI %} + +{% block head_script %} + {% include 'OroGridBundle:Include:stylesheet.html.twig' %} +{% endblock %} + +{% block page_container %} +
    + {% block content %} +
    +
    +
    + {% if validations.success is defined %} + {% for message in validations.success %} +
    +
    {{ message }}
    +
    + {% endfor %} + {% endif %} + {% if validations.error is defined %} + {% for message in validations.error %} +
    +
    {{ message }}
    +
    + {% endfor %} + {% endif %} +
    +
    +
    + + {{ UI.button({'path' : path('oro_entityconfig_view', {'id':entity.id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.button({'path' : path('oro_entityextend_update', {'id':entity.id}), 'title' : 'Cancel', 'label' : 'Proceed'}) }} + + {% endblock %} +
    +{% endblock %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/unique.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/unique.html.twig new file mode 100644 index 00000000000..9eebc5ed09f --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/unique.html.twig @@ -0,0 +1,87 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} +{% set title = 'Entitie Unique Keys' %} +{% set formAction = path('oro_entityextend_entity_unique_key', {id: entity_id}) %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_view', {id: entity_id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': 'Unique Keys', + 'additional': [ + { + 'indexPath' : path('oro_entityconfig_view', {id: entity_id}), + 'indexLabel' : entity_config.get('label')|default('N/A'), + }, + ] + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} + {{ parent() }} +{% endblock stats %} + +{% macro unique_collection_prototype(widget) %} + {% if 'prototype' in widget.vars|keys %} + {% set form = widget.vars.prototype %} + {% set name = widget.vars.prototype.vars.name %} + {% else %} + {% set form = widget %} + {% set name = widget.vars.full_name %} + {% endif %} +
    +
    + {{ form_errors(form) }} + {{ form_row(form.name) }} + {{ form_row(form.key) }} + +
    +
    +{% endmacro %} + +{% block unique_collection_widget %} + {% spaceless %} +
    +
    + {% for field in form.keys.children %} + {{ _self.unique_collection_prototype(field) }} + {% endfor %} +
    + {{ 'Add'|trans }} +
    + {% endspaceless %} +{% endblock unique_collection_widget %} + +{% block content_data %} + {% set id = 'configentity-unique' %} + {% set dataBlocks = [{ + 'title': 'Unique Keys', + 'class': 'active', + 'subblocks': [ + { + 'title': '', + 'useSpan': false, + 'data': [ + block('unique_collection_widget') + ] + } + ] + }] + %} + + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig new file mode 100644 index 00000000000..5294484f96e --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig @@ -0,0 +1,54 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} + +{% set title = 'Add Field ' %} +{% oro_title_set({params : {"%username%": 'User' }}) %} + +{% set formAction = path('oro_entityextend_field_create', {id: entity_id}) %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_view', {'id':entity_id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': 'New Field', + 'additional': [ + { + 'indexPath' : path('oro_entityconfig_view', {id: entity_id}), + 'indexLabel' : entity_config.get('label')|default('N/A') + }, + ] + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'configfield-create' %} + {% set dataBlocks = [ + { + 'title': 'Doctrine Type', + 'class': 'active', + 'subblocks': [{ + 'title': '', + 'data': [ + form_row(form.code), + form_row(form.type), + ] + }] + } + ] %} + + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + } %} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php b/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php new file mode 100644 index 00000000000..be3cdda2e8d --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php @@ -0,0 +1,326 @@ +backend = $backend; + $this->entityCacheDir = $entityCacheDir; + $this->configProvider = $configProvider; + } + + /** + * @param $entityName + * @param bool $force + */ + public function checkEntityCache($entityName, $force = false) + { + $extendClass = $this->generateExtendClassName($entityName); + $proxyClass = $this->generateProxyClassName($entityName); + + $validator = $this->entityCacheDir. DIRECTORY_SEPARATOR . 'validator.yml'; + if (!file_exists($validator)) { + file_put_contents( + $validator, + '' + ); + } + //$validatorYml = Yaml::parse($validator); + + if ((!class_exists($extendClass) || !class_exists($proxyClass)) || $force) { + /** write Dynamic class */ + file_put_contents( + $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $extendClass) . '.php', + "generateDynamicClass($entityName, $extendClass) + ); + + /** write Dynamic yml */ + file_put_contents( + $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $extendClass) . '.orm.yml', + Yaml::dump($this->generateDynamicYml($entityName, $extendClass), 5) + ); + + /** write Proxy class */ + file_put_contents( + $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $proxyClass) . '.php', + "generateProxyClass($entityName, $proxyClass) + ); + } + } + + public function generateExtendClassName($entityName) + { + return 'Extend\\Entity\\' . $this->backend . '\\' . $this->generateClassName($entityName); + } + + public function generateProxyClassName($entityName) + { + return 'Extend\\Proxy\\' . $this->generateClassName($entityName); + } + + protected function generateClassName($entityName) + { + return str_replace('\\', '', $entityName); + } + + protected function generateDynamicYml($entityName, $extendClass) + { + $yml = array( + $extendClass => array( + 'type' => 'entity', + 'table' => 'oro_extend_' . strtolower(str_replace('\\', '', $entityName)), + + 'oneToOne' => array( + '__extend__parent' => array( + 'targetEntity' => $entityName, + 'joinColumn' => array( + 'name' => '__extend__parent_id', + 'referencedColumnName' => 'id', + 'nullable' => true, + ), + ), + ), + 'fields' => array( + 'id' => array( + 'type' => 'integer', + 'id' => true, + 'generator' => array( + 'strategy' => 'AUTO' + ) + ) + ) + ) + ); + + if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { + foreach ($fields as $field => $options) { + if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { + $yml[$extendClass]['fields'][$field]['code'] = $options->getCode(); + $yml[$extendClass]['fields'][$field]['type'] = $options->getType(); + + $fieldConfig = $this->configProvider->getFieldConfig($entityName, $field); + + $yml[$extendClass]['fields'][$field]['length'] = $fieldConfig->get('length'); + $yml[$extendClass]['fields'][$field]['precision'] = $fieldConfig->get('precision'); + $yml[$extendClass]['fields'][$field]['scale'] = $fieldConfig->get('scale'); + } + } + } + + return $yml; + } + + protected function generateClassMethod($methodName, $methodBody, $methodArgs = array()) + { + $this->writer->reset(); + $method = PhpMethod::create($methodName)->setBody( + $this->writer->write($methodBody)->getContent() + ); + + if (count($methodArgs)) { + foreach ($methodArgs as $arg) { + $method->addParameter(PhpParameter::create($arg)); + } + } + + return $method; + } + + /** + * Prepare Dynamic class + * + * @param $entityName + * @param $className + * @return $this + */ + protected function generateDynamicClass($entityName, $className) + { + $this->writer = new Writer(); + + $class = PhpClass::create($this->generateClassName($entityName)) + ->setName($className) + ->setInterfaceNames(array('Oro\Bundle\EntityExtendBundle\Entity\ExtendEntityInterface')) + ->setProperty(PhpProperty::create('id')->setVisibility('protected')) + ->setProperty(PhpProperty::create('__extend__parent')->setVisibility('protected')) + ->setMethod( + $this->generateClassMethod( + 'getId', + 'return $this->id;' + ) + ) + ->setMethod( + $this->generateClassMethod( + '__extend__getParent', + 'return $this->__extend__parent;' + ) + ) + ->setMethod( + $this->generateClassMethod( + '__extend__setParent', + '$this->__extend__parent = $parent;return $this;', + array('parent') + ) + ) + ->setMethod( + $this->generateClassMethod( + '__fromArray', + 'foreach ($values as $key => $value) {$this->{\'set\'.ucfirst($key)}($value);}', + array('values') + ) + ); + + $toArray = ''; + if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { + foreach ($fields as $field => $options) { + if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { + $class + ->setProperty(PhpProperty::create($field)->setVisibility('protected')) + ->setMethod( + $this->generateClassMethod( + 'get'.ucfirst($field), + 'return $this->'.$field.';' + ) + ) + ->setMethod( + $this->generateClassMethod( + 'set'.ucfirst($field), + '$this->'.$field.' = $value; return $this;', + array('value') + ) + ); + $toArray .= ' \''.$field.'\' => $this->'.$field.','."\n"; + } + } + } + + $class->setMethod( + $this->generateClassMethod( + '__toArray', + 'return array('.$toArray."\n".');' + ) + ); + + $strategy = new DefaultGeneratorStrategy(); + + return $strategy->generate($class); + } + + /** + * Generate Proxy class + * + * @param $entityName + * @param $className + * @return $this + */ + protected function generateProxyClass($entityName, $className) + { + $this->writer = new Writer(); + + $class = PhpClass::create($this->generateClassName($entityName)) + ->setName($className) + ->setParentClassName($entityName) + ->setInterfaceNames(array('Oro\Bundle\EntityExtendBundle\Entity\ExtendProxyInterface')) + ->setProperty(PhpProperty::create('__proxy__extend')->setVisibility('protected')) + ->setMethod( + $this->generateClassMethod( + '__proxy__setExtend', + '$this->__proxy__extend = $extend;return $this;', + array('extend') + ) + ) + ->setMethod( + $this->generateClassMethod( + '__proxy__getExtend', + 'return $this->__proxy__extend;' + ) + ) + ->setMethod( + $this->generateClassMethod( + '__proxy__createFromEntity', + '$proxy=get_object_vars($entity);foreach ($proxy as $key=>$value){$this->$key=$value;}', + array('entity') + ) + ) + ->setMethod( + $this->generateClassMethod( + '__proxy__cloneToEntity', + '$proxy=get_object_vars($entity);foreach ($proxy as $key=>$value){$entity->$key=$this->$key;}', + array('entity') + ) + ); + + $toArray = ''; + if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { + foreach ($fields as $field => $options) { + if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { + $class->setMethod( + $this->generateClassMethod( + 'set'.ucfirst($field), + '$this->__proxy__extend->set'.ucfirst($field).'($'.$field.'); return $this;', + array($field) + ) + ); + $class->setMethod( + $this->generateClassMethod( + 'get'.ucfirst($field), + 'return $this->__proxy__extend->get'.ucfirst($field).'();' + ) + ); + + $toArray .= ' \''.$field.'\' => $this->__proxy__extend->get'.ucfirst($field).'(),'."\n"; + } else { + $toArray .= ' \''.$field.'\' => $this->get'.ucfirst($field).'(),'."\n"; + } + } + } + + $class->setMethod( + $this->generateClassMethod( + '__proxy__toArray', + 'return array('.$toArray."\n".');' + ) + ); + + $strategy = new DefaultGeneratorStrategy(); + + return $strategy->generate($class); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php new file mode 100644 index 00000000000..1f5d6eb1ff4 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php @@ -0,0 +1,114 @@ +em = $em; + $this->backend = $backend; + $this->extendManager = $extendManager; + } + + /** + * @return bool + */ + public function checkDynamicBackend() + { + try { + $this->exec("CREATE TABLE `__check_table__`(id INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(id))"); + $this->exec("DROP TABLE `__check_table__`"); + + return true; + } catch (\Exception $e) { + return false; + } + } + + public function checkIsSynchronized($table) + { + try { + + } catch (\Exception $e) { + return false; + } + } + + /** + * @param $field FieldConfig + * @return bool + */ + public function checkFieldIsSystem(FieldConfig $field) + { + $isSystem = false; + $metadata = $this->em->getClassMetadata($field->getClassName()); + if (in_array($field->getCode(), $metadata->fieldNames)) { + $isSystem = true; + } + + return $isSystem; + } + + /** + * @param $field FieldConfig + * @return bool + */ + public function checkFieldCanDelete(FieldConfig $field) + { + $canDelete = false; + + if ($field->getClassName() + && $field->getCode() + && !$this->checkFieldIsSystem($field) + ) { + $extendClass = $this->extendManager->getExtendClass($field->getClassName()); + + /** @var QueryBuilder $builder */ + $builder = $this->em->getRepository($extendClass)->createQueryBuilder('ex'); + $builder->select('MAX(ex.'.$field->getCode(). ')'); + + if (!$builder->getQuery()->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)) { + $canDelete = true; + } + } + + return $canDelete; + } + + /** + * @param $statement + * @return int + */ + protected function exec($statement) + { + return $this->em->getConnection()->exec($statement); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/composer.json b/src/Oro/Bundle/EntityExtendBundle/composer.json new file mode 100644 index 00000000000..c09fa8c1de8 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/composer.json @@ -0,0 +1,30 @@ +{ + "name": "oro/entity-extend-bundle", + "type": "symfony-bundle", + "description": "BAP Entity Extend Bundle", + "keywords": ["BAP"], + "homepage": "https://github.com/laboro/EntityExtendBundle", + "license": "MIT", + "require": { + "php": ">=5.3.3", + "symfony/symfony": "2.3.*", + "oro/ui-bundle": "dev-master", + "oro/grid-bundle": "dev-master", + "oro/navigation-bundle": "dev-master", + "oro/data-audit-bundle": "dev-master", + "oro/form-bundle": "dev-master", + "oro/user-bundle": "dev-master", + "oro/entity-bundle": "dev-master", + "oro/entity-config-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\EntityExtendBundle": "" } + }, + "target-dir": "Oro/Bundle/EntityExtendBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/css/oro.filter.css b/src/Oro/Bundle/FilterBundle/Resources/public/css/oro.filter.css index b0b0c0e4727..3be9d6ad137 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/css/oro.filter.css +++ b/src/Oro/Bundle/FilterBundle/Resources/public/css/oro.filter.css @@ -33,7 +33,7 @@ padding: 8px 8px 2px 8px; border: 1px solid #d3d3d3; display: none; - z-index: 990; + z-index: 700; left: 5px; } .filter-select .select-filter-widget { diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/choicefilter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/choicefilter.js index 12bd8108491..25b364a73ad 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/choicefilter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/choicefilter.js @@ -31,7 +31,7 @@ Oro.Filter.ChoiceFilter = Oro.Filter.TextFilter.extend({ '
    ' + '
    ' + - '' + + '' + '' + '' ), diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/datefilter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/datefilter.js index 15a17bcc0b7..0ef1d3a8623 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/datefilter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/datefilter.js @@ -29,7 +29,7 @@ Oro.Filter.DateFilter = Oro.Filter.ChoiceFilter.extend({ '' + '
    ' + '
    ' + - '' + + '' + '
    ' + '
    ' + '' diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/multiselectfilter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/multiselectfilter.js index cb7f93aea2c..79be40e8529 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/multiselectfilter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/multiselectfilter.js @@ -20,7 +20,7 @@ Oro.Filter.MultiSelectFilter = Oro.Filter.SelectFilter.extend({ '<% _.each(options, function (hint, value) { %><% }); %>' + '' + '' + - 'Close' + '<%- _.__("Close") %>' ), /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/selectfilter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/selectfilter.js index 09bd742b047..04c873e0703 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/selectfilter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/selectfilter.js @@ -21,7 +21,7 @@ Oro.Filter.SelectFilter = Oro.Filter.AbstractFilter.extend({ '<% _.each(options, function (hint, value) { %><% }); %>' + '' + '' + - 'Close' + '<%- _.__("Close") %>' ), /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/textfilter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/textfilter.js index 65b70965a6c..afefe4bdee8 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/textfilter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/app/filter/textfilter.js @@ -18,7 +18,7 @@ Oro.Filter.TextFilter = Oro.Filter.AbstractFilter.extend({ '<%= label %>: <%= criteriaHint %>' + '' + '' + - 'Close' + + '<%- _.__("Close") %>' + '' + '
    ' + '
    ' + - '' + + '' + '
    ' + '
    ' + '' diff --git a/src/Oro/Bundle/FilterBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/FilterBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..ccdc0010103 --- /dev/null +++ b/src/Oro/Bundle/FilterBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,2 @@ +"Close": "Close" +"Update": "Update" diff --git a/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php b/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php index 973fe03576a..e282d574cb3 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/AttributeType/AbstractAttributeType.php @@ -1,4 +1,5 @@ qb->expr()->eq($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->eq($field, $this->expr()->literal($value))->__toString(); break; case '<': - $condition = $this->qb->expr()->lt($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->lt($field, $this->expr()->literal($value))->__toString(); break; case '<=': - $condition = $this->qb->expr()->lte($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->lte($field, $this->expr()->literal($value))->__toString(); break; case '>': - $condition = $this->qb->expr()->gt($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->gt($field, $this->expr()->literal($value))->__toString(); break; case '>=': - $condition = $this->qb->expr()->gte($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->gte($field, $this->expr()->literal($value))->__toString(); break; case 'LIKE': - $condition = $this->qb->expr()->like($field, $this->qb->expr()->literal($value))->__toString(); + $condition = $this->expr()->like($field, $this->expr()->literal($value))->__toString(); break; case 'NOT LIKE': - $condition = sprintf('%s NOT LIKE %s', $field, $this->qb->expr()->literal($value)); + $condition = sprintf('%s NOT LIKE %s', $field, $this->expr()->literal($value)); break; case 'NULL': - $condition = $this->qb->expr()->isNull($field); + $condition = $this->expr()->isNull($field); break; case 'NOT NULL': - $condition = $this->qb->expr()->isNotNull($field); + $condition = $this->expr()->isNotNull($field); break; case 'IN': - $condition = $this->qb->expr()->in($field, $value)->__toString(); + $condition = $this->expr()->in($field, $value)->__toString(); break; case 'NOT IN': - $condition = $this->qb->expr()->notIn($field, $value)->__toString(); + $condition = $this->expr()->notIn($field, $value)->__toString(); break; default: throw new FlexibleQueryException('operator '.$operator.' is not supported'); @@ -282,26 +282,13 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val // inner join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->qb->innerJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->innerJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then join to option with filter on option id $joinAliasOpt = 'filterO'.$attribute->getCode().$this->aliasCounter; $backendField = sprintf('%s.%s', $joinAliasOpt, 'id'); $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); - $this->qb->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); - - } else if ($attribute->getBackendType() == AbstractAttributeType::BACKEND_TYPE_ENTITY) { - - // inner join to value - $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $rootAlias = $this->qb->getRootAliases(); - $this->qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - - // then join to linked entity with filter on id - $joinAliasOpt = 'filterentity'.$attribute->getCode().$this->aliasCounter; - $backendField = sprintf('%s.id', $joinAliasEntity); - $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); - $this->qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); + $this->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); } else { @@ -309,7 +296,7 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val $backendField = sprintf('%s.%s', $joinAlias, $attribute->getBackendType()); $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); $condition .= ' AND '.$this->prepareCriteriaCondition($backendField, $operator, $value); - $this->qb->innerJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->innerJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); } return $this; @@ -331,26 +318,25 @@ public function addAttributeOrderBy(AbstractAttribute $attribute, $direction) // join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->qb->leftJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->leftJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then to option and option value to sort on $joinAliasOpt = $aliasPrefix.'O'.$attribute->getCode().$this->aliasCounter; $condition = $joinAliasOpt.".attribute = ".$attribute->getId(); - $this->qb->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + $this->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); $joinAliasOptVal = $aliasPrefix.'OV'.$attribute->getCode().$this->aliasCounter; - $condition = $joinAliasOptVal.'.locale = '.$this->qb->expr()->literal($this->getLocale()); - $this->qb->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); + $condition = $joinAliasOptVal.'.locale = '.$this->expr()->literal($this->getLocale()); + $this->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); - $this->qb->addOrderBy($joinAliasOpt.'.defaultValue', $direction); - $this->qb->addOrderBy($joinAliasOptVal.'.value', $direction); + $this->addOrderBy($joinAliasOptVal.'.value', $direction); } else { // join to value and sort on $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->qb->leftJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - $this->qb->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); + $this->leftJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); } } } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Attribute.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Attribute.php index f9e0dabd85a..27dc586170a 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Attribute.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Attribute.php @@ -1,4 +1,5 @@ data = $data; - + return $this; } /** * Get data * - * @return string + * @return string */ public function getData() { @@ -85,19 +87,20 @@ public function getData() * Set type * * @param integer $type + * * @return Collection */ public function setType($type) { $this->type = $type; - + return $this; } /** * Get type * - * @return integer + * @return integer */ public function getType() { diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityAttribute.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityAttribute.php index 6cd82393038..7835ada34e2 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityAttribute.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityAttribute.php @@ -1,4 +1,5 @@ values; + if (!isset($this->values) || !$this->values->count()) { + return $this->values; + } + + $collection = new ArrayCollection(); + foreach ($this->values as $value) { + $collection[$value->getAttribute()->getCode()] = $value; + } + + return $collection; } /** @@ -237,6 +245,7 @@ function ($value) use ($name) { } } ); + return (count($values) >= 1); } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php index 623a2ee6b05..36a0b95caf0 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php @@ -1,4 +1,5 @@ */ class AttributeOptionRepository extends EntityRepository { diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/AttributeRepository.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/AttributeRepository.php index de7e541adf0..2d67000e7c8 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/AttributeRepository.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Repository/AttributeRepository.php @@ -1,4 +1,5 @@ entityToManager[$realClassName])) { throw new \InvalidArgumentException(sprintf('Cannot get flexible manager for class "%s".', $entityFQCN)); } + return $this->entityToManager[$realClassName]; } } diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Manager/SimpleManager.php b/src/Oro/Bundle/FlexibleEntityBundle/Manager/SimpleManager.php index 374cf434479..b3c245ae736 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Manager/SimpleManager.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Manager/SimpleManager.php @@ -1,4 +1,5 @@ assertEquals($value, call_user_func_array(array($obj, 'get' . ucfirst($property)), array())); } + /** + * Test related method + */ public function testToString() { $obj = new Collection(); $text = 'sfd'; $obj->setData($text); - $this->assertEquals((string)$obj, $text); + $this->assertEquals((string) $obj, $text); } /** diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Demo/Flexible.php b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Demo/Flexible.php index 0843ed12abb..cedbe2780ae 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Demo/Flexible.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Entity/Demo/Flexible.php @@ -1,4 +1,5 @@ isEmpty() || empty($attributes)) { $values = $skip ? new ArrayCollection() : $values; + return $values; } diff --git a/src/Oro/Bundle/FormBundle/Config/BlockConfig.php b/src/Oro/Bundle/FormBundle/Config/BlockConfig.php new file mode 100644 index 00000000000..8e4eaf6889b --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Config/BlockConfig.php @@ -0,0 +1,215 @@ +code = $code; + } + + /** + * @param mixed $blockConfig + * @return $this + */ + public function setBlockConfig($blockConfig) + { + $this->blockConfig = $blockConfig; + + return $this; + } + + /** + * @return mixed + */ + public function getBlockConfig() + { + return $this->blockConfig; + } + + /** + * @param $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @param $title + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param $class + * @return $this + */ + public function setClass($class) + { + $this->class = $class; + + return $this; + } + + /** + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * @param int $priority + * @return $this + */ + public function setPriority($priority) + { + $this->priority = $priority; + + return $this; + } + + /** + * @return int + */ + public function getPriority() + { + return $this->priority; + } + + /** + * @param SubBlockConfig $config + * @return $this + */ + public function addSubBlock(SubBlockConfig $config) + { + $this->subBlocks[$config->getCode()] = $config; + + $this->sortSubBlocks(); + + return $this; + } + + /** + * @param $subBlocks + * @return $this + */ + public function setSubBlocks($subBlocks) + { + $this->subBlocks = $subBlocks; + + $this->sortSubBlocks(); + + return $this; + } + + /** + * @return array|SubBlockConfig + */ + public function getSubBlocks() + { + return $this->subBlocks; + } + + /** + * @param $code + * @return SubBlockConfig + */ + public function getSubBlock($code) + { + return $this->subBlocks[$code]; + } + + /** + * @param $code + * @return boolean + */ + public function hasSubBlock($code) + { + return isset($this->subBlocks[$code]); + } + + /** + * @return array + */ + public function toArray() + { + return array( + 'title' => $this->title, + 'class' => $this->class, + 'subblocks' => array_map(function (SubBlockConfig $config) { + return $config->toArray(); + }, $this->subBlocks) + ); + } + + protected function sortSubBlocks() + { + $priority = array(); + foreach ($this->subBlocks as $key => $subBlock) { + $priority[$key] = $subBlock->getPriority(); + } + + array_multisort($priority, SORT_DESC, $this->subBlocks); + } +} diff --git a/src/Oro/Bundle/FormBundle/Config/FormConfig.php b/src/Oro/Bundle/FormBundle/Config/FormConfig.php new file mode 100644 index 00000000000..30f733a08cb --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Config/FormConfig.php @@ -0,0 +1,93 @@ +blocks[$block->getCode()] = $block; + + $this->sortBlocks(); + + return $this; + } + + /** + * @param $code + * @return BlockConfig + */ + public function getBlock($code) + { + return $this->blocks[$code]; + } + + /** + * @param $code + * @return bool + */ + public function hasBlock($code) + { + return isset($this->blocks[$code]); + } + + /** + * @return array|BlockConfig + */ + public function getBlocks() + { + return $this->blocks; + } + + /** + * @param $blocks + * @return $this + */ + public function setBlocks($blocks) + { + $this->blocks = $blocks; + + $this->sortBlocks(); + + return $this; + } + + /** + * @param $blockCode + * @param $subBlockIndex + * @return SubBlockConfig + */ + public function getSubBlocks($blockCode, $subBlockIndex) + { + return $this->getBlock($blockCode)->getSubBlock($subBlockIndex); + } + + /** + * @return array + */ + public function toArray() + { + return array_map(function (BlockConfig $block) { + return $block->toArray(); + }, $this->blocks); + } + + protected function sortBlocks() + { + $priority = array(); + foreach ($this->blocks as $key => $block) { + $priority[$key] = $block->getPriority(); + } + + array_multisort($priority, SORT_DESC, $this->blocks); + } +} diff --git a/src/Oro/Bundle/FormBundle/Config/FormConfigInterface.php b/src/Oro/Bundle/FormBundle/Config/FormConfigInterface.php new file mode 100644 index 00000000000..f84e72cb467 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Config/FormConfigInterface.php @@ -0,0 +1,8 @@ +code = $code; + } + + /** + * @param $title + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @param int $priority + * @return $this + */ + public function setPriority($priority) + { + $this->priority = $priority; + + return $this; + } + + /** + * @return int + */ + public function getPriority() + { + return $this->priority; + } + + /** + * @param $data + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * @param $data + * @return $this + */ + public function addData($data) + { + $this->data[] = $data; + + return $this; + } + + /** + * @return array + */ + public function toArray() + { + return array( + 'code' => $this->code, + 'title' => $this->title, + 'data' => $this->data + ); + } +} diff --git a/src/Oro/Bundle/FormBundle/DependencyInjection/OroFormExtension.php b/src/Oro/Bundle/FormBundle/DependencyInjection/OroFormExtension.php index b0c7e7c78a2..bea7aa0c4ba 100644 --- a/src/Oro/Bundle/FormBundle/DependencyInjection/OroFormExtension.php +++ b/src/Oro/Bundle/FormBundle/DependencyInjection/OroFormExtension.php @@ -22,5 +22,6 @@ public function load(array $configs, ContainerBuilder $container) $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('autocomplete.yml'); $loader->load('form_type.yml'); + $loader->load('services.yml'); } } diff --git a/src/Oro/Bundle/FormBundle/Exception/RuntimeException.php b/src/Oro/Bundle/FormBundle/Exception/RuntimeException.php new file mode 100644 index 00000000000..4e027dd443d --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Exception/RuntimeException.php @@ -0,0 +1,7 @@ +setOptional(array( + 'block', + 'subblock', + 'block_config' + )); + } + + /** + * @param FormView $view + * @param FormInterface $form + * @param array $options + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + if (isset($options['block'])) { + $view->vars['block'] = $options['block']; + } + + if (isset($options['subblock'])) { + $view->vars['subblock'] = $options['subblock']; + } + + if (isset($options['block_config'])) { + $view->vars['block_config'] = $options['block_config']; + } + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Oro/Bundle/FormBundle/Form/Twig/DataBlocks.php b/src/Oro/Bundle/FormBundle/Form/Twig/DataBlocks.php new file mode 100644 index 00000000000..8e57fc395a1 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Form/Twig/DataBlocks.php @@ -0,0 +1,178 @@ +accessor = PropertyAccess::createPropertyAccessor(); + } + + /** + * @param \Twig_Environment $env + * @param $context + * @param FormView $form + * @param string $formVariableName + * @return array + */ + public function render(\Twig_Environment $env, $context, FormView $form, $formVariableName = 'form') + { + $this->formVariableName = $formVariableName; + $this->formConfig = new FormConfig; + $this->context = $context; + $this->env = $env; + + $tmpLoader = $env->getLoader(); + $env->setLoader(new \Twig_Loader_Chain(array($tmpLoader, new \Twig_Loader_String()))); + + $this->renderBlock($form); + + $env->setLoader($tmpLoader); + + return $this->formConfig->toArray(); + } + + /** + * @param FormView $form + */ + protected function renderBlock(FormView $form) + { + if (isset($form->vars['block_config'])) { + foreach ($form->vars['block_config'] as $code => $blockConfig) { + $this->createBlock($code, $blockConfig); + } + } + + foreach ($form->children as $name => $child) { + if (isset($child->vars['block']) || isset($child->vars['subblock'])) { + + $block = null; + if ($this->formConfig->hasBlock($child->vars['block'])) { + $block = $this->formConfig->getBlock($child->vars['block']); + } + + if (!$block) { + $blockCode = $child->vars['block']; + $block = $this->createBlock($blockCode); + + $this->formConfig->addBlock($block); + } + + $subBlock = $this->getSubBlock($name, $child, $block); + + $tmpChild = $child; + $formPath = ''; + + while ($tmpChild->parent) { + $formPath = sprintf('.children[\'%s\']', $tmpChild->vars['name']) . $formPath; + $tmpChild = $tmpChild->parent; + } + + $subBlock->addData($this->env->render( + '{{ form_row(' . $this->formVariableName . $formPath . ') }}', + $this->context + )); + } + + $this->renderBlock($child); + } + } + + protected function getSubBlock($name, FormView $child, BlockConfig $block) + { + $subBlock = null; + if (isset($child->vars['subblock']) && $block->hasSubBlock($child->vars['subblock'])) { + $subBlock = $block->getSubBlock($child->vars['subblock']); + } elseif (!isset($child->vars['subblock'])) { + $subBlocks = $block->getSubBlocks(); + $subBlock = reset($subBlocks); + } + + if (!$subBlock) { + if (isset($child->vars['subblock'])) { + $subBlockCode = $child->vars['subblock']; + } else { + $subBlockCode = $name . '__subblock'; + } + + $subBlock = $this->createSubBlock($subBlockCode, array('title' => null)); + $block->addSubBlock($subBlock); + } + + return $subBlock; + } + + /** + * @param $code + * @param array $blockConfig + * @return BlockConfig + */ + protected function createBlock($code, $blockConfig = array()) + { + $block = new BlockConfig($code); + $block->setClass($this->accessor->getValue($blockConfig, '[class]')); + $block->setPriority($this->accessor->getValue($blockConfig, '[priority]')); + + $title = $this->accessor->getValue($blockConfig, '[title]') + ? $this->accessor->getValue($blockConfig, '[title]') + : ucfirst($code); + $block->setTitle($title); + + foreach ((array)$this->accessor->getValue($blockConfig, '[subblocks]') as $subCode => $subBlockConfig) { + $block->addSubBlock($this->createSubBlock($subCode, (array)$subBlockConfig)); + } + + $this->formConfig->addBlock($block); + + return $block; + } + + /** + * @param $code + * @param $config + * @return SubBlockConfig + */ + protected function createSubBlock($code, $config) + { + $subBlock = new SubBlockConfig($code); + $subBlock->setTitle($this->accessor->getValue($config, '[title]')); + $subBlock->setPriority($this->accessor->getValue($config, '[priority]')); + + return $subBlock; + } +} diff --git a/src/Oro/Bundle/FormBundle/Form/Type/OroJquerySelect2HiddenType.php b/src/Oro/Bundle/FormBundle/Form/Type/OroJquerySelect2HiddenType.php index e2aa0b1c892..ab20aafcf72 100644 --- a/src/Oro/Bundle/FormBundle/Form/Type/OroJquerySelect2HiddenType.php +++ b/src/Oro/Bundle/FormBundle/Form/Type/OroJquerySelect2HiddenType.php @@ -184,9 +184,19 @@ public function buildView(FormView $view, FormInterface $form, array $options) parent::buildView($view, $form, $options); $vars = array('configs' => $options['configs']); + if ($form->getData()) { + $result = array(); + if (isset($options['configs']['multiple']) && $options['configs']['multiple']) { + foreach ($form->getData() as $item) { + $result[] = $options['converter']->convertItem($item); + } + } else { + $result[] = $options['converter']->convertItem($form->getData()); + } + $vars['attr'] = array( - 'data-entity' => json_encode($options['converter']->convertItem($form->getData())) + 'data-entities' => json_encode($result) ); } diff --git a/src/Oro/Bundle/FormBundle/Resources/config/assets.yml b/src/Oro/Bundle/FormBundle/Resources/config/assets.yml index 8aff8e6fd85..5785639b411 100644 --- a/src/Oro/Bundle/FormBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/FormBundle/Resources/config/assets.yml @@ -1,3 +1,3 @@ js: 'form': - - '@OroFormBundle/Resources/public/js/oro.select2.config.js' \ No newline at end of file + - '@OroFormBundle/Resources/public/js/oro.select2.config.js' diff --git a/src/Oro/Bundle/FormBundle/Resources/config/form_type.yml b/src/Oro/Bundle/FormBundle/Resources/config/form_type.yml index 3bb342f78b1..ed3f13d999e 100644 --- a/src/Oro/Bundle/FormBundle/Resources/config/form_type.yml +++ b/src/Oro/Bundle/FormBundle/Resources/config/form_type.yml @@ -1,9 +1,10 @@ parameters: - oro_form.type.date.class: Oro\Bundle\FormBundle\Form\Type\OroDateType - oro_form.type.datetime.class: Oro\Bundle\FormBundle\Form\Type\OroDateTimeType - oro_form.type.combobox_local.class: Oro\Bundle\FormBundle\Form\Type\OroComboboxLocalType - oro_form.type.entity_identifier.class: Oro\Bundle\FormBundle\Form\Type\EntityIdentifierType - oro_form.type.jqueryselect2_hidden.class: Oro\Bundle\FormBundle\Form\Type\OroJquerySelect2HiddenType + oro_form.type.date.class: Oro\Bundle\FormBundle\Form\Type\OroDateType + oro_form.type.datetime.class: Oro\Bundle\FormBundle\Form\Type\OroDateTimeType + oro_form.type.combobox_local.class: Oro\Bundle\FormBundle\Form\Type\OroComboboxLocalType + oro_form.type.entity_identifier.class: Oro\Bundle\FormBundle\Form\Type\EntityIdentifierType + oro_form.type.jqueryselect2_hidden.class: Oro\Bundle\FormBundle\Form\Type\OroJquerySelect2HiddenType + oro_form.extension.data_block.class: Oro\Bundle\FormBundle\Form\Extension\DataBlockExtension services: # Form types @@ -30,3 +31,8 @@ services: - @oro_form.autocomplete.search_registry tags: - { name: form.type, alias: oro_jqueryselect2_hidden } + + oro_form.extension.data_block: + class: %oro_form.extension.data_block.class% + tags: + - { name: form.type_extension, alias: form } diff --git a/src/Oro/Bundle/FormBundle/Resources/config/services.yml b/src/Oro/Bundle/FormBundle/Resources/config/services.yml new file mode 100644 index 00000000000..d7cc3767f25 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Resources/config/services.yml @@ -0,0 +1,7 @@ +parameters: + oro_form.twig.form.class: Oro\Bundle\FormBundle\Twig\FormExtension +services: + oro_form.twig.form_extension: + class: %oro_form.twig.form.class% + tags: + - { name: twig.extension } diff --git a/src/Oro/Bundle/FormBundle/Resources/doc/index.md b/src/Oro/Bundle/FormBundle/Resources/doc/index.md index 65145cf3382..97036cb107d 100644 --- a/src/Oro/Bundle/FormBundle/Resources/doc/index.md +++ b/src/Oro/Bundle/FormBundle/Resources/doc/index.md @@ -3,3 +3,4 @@ OroFormBundle Documentation - [Form Components Overview](./reference/form_components.md) - [Autocomplete Form Type](./reference/autocomplete_form_type.md) +- [UI DataBlock Config](./reference/ui_datablock_config.md) diff --git a/src/Oro/Bundle/FormBundle/Resources/doc/reference/ui_datablock_config.md b/src/Oro/Bundle/FormBundle/Resources/doc/reference/ui_datablock_config.md new file mode 100644 index 00000000000..e1c612f7b88 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Resources/doc/reference/ui_datablock_config.md @@ -0,0 +1,99 @@ +UI DataBlock Config Overview +============================ + +This functionality add ability to config DataBlocks for UI component inside FromType instead of template + + +Configure block in template: +---------------------------- + +update.html.twig + +``` +Twig +{% set dataBlocks = [{ + 'title': 'First Block', + 'class': '', + 'subblocks': [ + { + 'title': '', + 'data': [ + form_row(form.name), + form_row(form.age) + ] + }, + { + 'title': 'Email SubBlock', + 'data': [ + form_row(form.email), + ] + } + ] + }] + +%} +``` + + +Configure block in FormType +--------------------------- + +``` +Php +class UserType extends AbstractType +{ + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('name', 'text', array('block' => 'first' )); + $builder->add('age', 'integer', array('block' => 'first', 'subblock' => 'first')); + $builder->add('email', 'email', array('block' => 'second')); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'block_config' => array( + 'first' => array( + 'priority' => 2, + 'title' => 'First Block', + 'subblocks' => array( + 'first' => array(), + 'second' => array( + 'title' => 'Email SubBlock' + ), + ), + ), + ), + ) + ); + } +} + + +Twig +{% set dataBlocks = form_data_blocks(form) %} + +``` + + +'block' - code of block, +If block is not configured in 'block_config'. Block will be created. +If block title is not configured in 'block_config'. Title of block will be same as code. +If form type filed options don't have 'block' attribute, this filed will be ignored + +'subblock' - code of subblock, +If subblock is not configured in 'block_config'. SubBlock will be created. +If form type filed options don't have 'subblock' attribute, this field will be added to first subblock in block + +If 'subblock' is congigured but 'block' is not configured field will be ignored + + +'block_config' is optinal attribute +This attribute contain config for block and subblock(title, class, subblocks) \ No newline at end of file diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/oro.select2.config.js b/src/Oro/Bundle/FormBundle/Resources/public/js/oro.select2.config.js index 94258d63bba..5e6abe5bb77 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/oro.select2.config.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/oro.select2.config.js @@ -13,7 +13,7 @@ OroSelect2Config.prototype.getConfig = function () { this.config.formatSelection = this.format(this.config.selection_template !== undefined ? this.config.selection_template : false); } if (this.config.initSelection === undefined) { - this.config.initSelection = this.initSelection; + this.config.initSelection = _.bind(this.initSelection, this); } if (this.config.ajax === undefined) { this.config.ajax = { @@ -63,7 +63,11 @@ OroSelect2Config.prototype.format = function (jsTemplate) { }; OroSelect2Config.prototype.initSelection = function (element, callback) { - callback(element.data('entity')); + if (!_.isUndefined(this.config.multiple) && this.config.multiple === true) { + callback(element.data('entities')); + } else { + callback(element.data('entities').pop()); + } }; OroSelect2Config.prototype.highlightSelection = function (str, selection) { diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Autocomplete/SearchHandlerTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Autocomplete/SearchHandlerTest.php index 3b5e01591af..476693120f5 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Autocomplete/SearchHandlerTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Autocomplete/SearchHandlerTest.php @@ -202,13 +202,13 @@ public function testGetEntitName() /** * @dataProvider searchDataProvider * @param string $query - * @param array $expectedResult - * @param array $expectedIndexerCalls - * @param array $expectSearchResultCalls - * @param array $expectEntityRepositoryCalls - * @param array $expectQueryBuilderCalls - * @param array $expectExprCalls - * @param array $expectQueryCalls + * @param array $expectedResult + * @param array $expectedIndexerCalls + * @param array $expectSearchResultCalls + * @param array $expectEntityRepositoryCalls + * @param array $expectQueryBuilderCalls + * @param array $expectExprCalls + * @param array $expectQueryCalls */ public function testSearch( $query, @@ -372,7 +372,7 @@ public function getMockQuery() } /** - * @param array $ids + * @param array $ids * @return Item[] */ public function createMockSearchItems(array $ids) @@ -388,6 +388,7 @@ public function createMockSearchItems(array $ids) ->will($this->returnValue($id)); $result[] = $item; } + return $result; } @@ -397,6 +398,7 @@ public function createStubEntityWithProperties(array $data) foreach ($data as $name => $property) { $result->$name = $property; } + return $result; } @@ -412,6 +414,7 @@ public function createMockEntity(array $data) ->method($methods[$name]) ->will($this->returnValue($property)); } + return $result; } @@ -421,6 +424,7 @@ public function createMockFlexibleValue($data) $result->expects($this->any()) ->method('getData') ->will($this->returnValue($data)); + return $result; } } diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Config/BlockConfigTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Config/BlockConfigTest.php new file mode 100644 index 00000000000..643b5374968 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Config/BlockConfigTest.php @@ -0,0 +1,167 @@ + array( + 'type' => array( + 'title' => 'Doctrine Type', + 'priority' => 1, + 'subblocks' => array( + 'common' => array( + 'title' => 'Common Setting', + 'priority' => 1, + ), + 'custom' => array( + 'title' => 'Custom Setting', + 'priority' => 2, + ), + ) + ), + ) + ); + + private $testSubBlocks = array(); + + private $testSubBlocksConfig = array( + 'common' => array( + 'title' => 'Common Setting', + 'priority' => 3, + ), + 'custom' => array( + 'title' => 'Custom Setting', + 'priority' => 2, + ), + 'last' => array( + 'title' => 'Last SubBlock', + 'priority' => 1, + ) + ); + + public function setUp() + { + $this->blockConfig = new BlockConfig($this->blockCode); + } + + public function testProperties() + { + /** test getCode */ + $this->assertEquals($this->blockCode, $this->blockConfig->getCode()); + + /** test setCode */ + $this->blockConfig->setCode($this->testCode); + $this->assertEquals($this->testCode, $this->blockConfig->getCode()); + + /** test getTitle */ + $this->assertNull($this->blockConfig->getTitle()); + + /** test setTitle */ + $this->blockConfig->setTitle($this->testTitle); + $this->assertEquals($this->testTitle, $this->blockConfig->getTitle()); + + /** test getPriority */ + $this->assertNull($this->blockConfig->getPriority()); + + /** test setPriority */ + $this->blockConfig->setPriority(10); + $this->assertEquals(10, $this->blockConfig->getPriority()); + + /** test getClass */ + $this->assertNull($this->blockConfig->getClass()); + + /** test setClass */ + $this->blockConfig->setClass($this->testClass); + $this->assertEquals($this->testClass, $this->blockConfig->getClass()); + + /** test getSubBlock */ + $this->assertEquals(array(), $this->blockConfig->getSubBlocks()); + + /** test setSubBlocks */ + $this->blockConfig->setSubBlocks($this->testSubBlocks); + $this->assertEquals($this->testSubBlocks, $this->blockConfig->getSubBlocks()); + + /** test hasSubBlock */ + $this->assertFalse($this->blockConfig->hasSubBlock('testSubBlock')); + + /** test setSubBlock */ + $subblocks = array(); + foreach ($this->testSubBlocksConfig as $code => $data) { + $subblocks[] = array( + 'code' => $code, + 'title' => $data['title'], + 'data' => array('some_data') + ); + $subBlock = new SubBlockConfig($code); + + /** test SubBlockConfig set/get Title/Priority/Code */ + $subBlock->setTitle($data['title']); + $this->assertEquals($data['title'], $subBlock->getTitle()); + + $subBlock->setPriority($data['priority']); + $this->assertEquals($data['priority'], $subBlock->getPriority()); + + $subBlock->setCode($code); + $this->assertEquals($code, $subBlock->getCode()); + + $subBlock->setData(array('some_data')); + $this->assertEquals(array('some_data'), $subBlock->getData()); + + /** test SubBlockConfig addSubBlock */ + $this->blockConfig->addSubBlock($subBlock); + $this->assertEquals($subBlock, $this->blockConfig->getSubBlock($code)); + + $this->testSubBlocks[] = $subBlock; + } + + $this->blockConfig->setSubBlocks($this->testSubBlocks); + $this->assertEquals($this->testSubBlocks, $this->blockConfig->getSubBlocks()); + + $this->assertEquals( + array( + 'title' => $this->testTitle, + 'class' => $this->testClass, + 'subblocks' => $subblocks + ), + $this->blockConfig->toArray() + ); + } + + public function testException() + { + /** test getSubBlock Exception */ + $this->setExpectedException( + '\PHPUnit_Framework_Error_Notice', 'Undefined index: testSubBlock' + ); + $this->blockConfig->getSubBlock('testSubBlock'); + } + + public function testBlockConfig() + { + $this->assertNull($this->blockConfig->getBlockConfig()); + + $this->blockConfig->setBlockConfig($this->testBlockConfig); + $this->assertEquals( + $this->testBlockConfig, + $this->readAttribute($this->blockConfig, 'blockConfig') + ); + } +} diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Config/FormConfigTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Config/FormConfigTest.php new file mode 100644 index 00000000000..9297dedbae5 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Config/FormConfigTest.php @@ -0,0 +1,101 @@ + array( + 'title' => 'Common Setting', + 'priority' => 3, + ), + 'custom' => array( + 'title' => 'Custom Setting', + 'priority' => 2, + ), + 'last' => array( + 'title' => 'Last SubBlock', + 'priority' => 1, + ) + ); + + public function setUp() + { + $this->formConfig = new FormConfig(); + } + + public function testAddBlock() + { + /** test getBlocks without any adjusted block(s) */ + $this->assertEquals(array(), $this->formConfig->getBlocks()); + + /** test hasBlock without any adjusted block(s) */ + $this->assertFalse($this->formConfig->hasBlock('testBlock')); + + /** @var BlockConfig $blockConfig */ + $blockConfig = new BlockConfig('testBlock'); + $blockConfig + ->setTitle('Test Block') + ->setClass('Oro\Bundle\UserBundle\Entity\User') + ->setPriority(1); + + $subblocks = array(); + $subblocksArray = array(); + foreach ($this->testSubBlocksConfig as $code => $data) { + $subBlock = new SubBlockConfig($code); + $subBlock + ->setTitle($data['title']) + ->setPriority($data['priority']); + $blockConfig->addSubBlock($subBlock); + + $subblocks[] = $subBlock; + $subblocksArray[$code] = $subBlock->toArray(); + } + + $this->formConfig->addBlock($blockConfig); + $this->blocks[] = $blockConfig; + + /** test hasBlock */ + $this->assertTrue($this->formConfig->hasBlock('testBlock')); + + /** test getBlock */ + $this->assertEquals($blockConfig, $this->formConfig->getBlock('testBlock')); + + /** test getSubBlock */ + $this->assertEquals($subblocks[0], $this->formConfig->getSubBlocks('testBlock', 'common')); + + /** test toArray() */ + $this->assertEquals(array('testBlock' => array( + 'title' => 'Test Block', + 'class' => 'Oro\Bundle\UserBundle\Entity\User', + 'subblocks' => $subblocksArray + )), + $this->formConfig->toArray() + ); + + /** test getBlocks */ + $this->formConfig->setBlocks($this->blocks); + $this->assertEquals( + $this->blocks, + $this->formConfig->getBlocks() + ); + } + + public function testException() + { + /** test getSubBlock Exception */ + $this->setExpectedException( + '\PHPUnit_Framework_Error_Notice', 'Undefined index: testBlock' + ); + $this->formConfig->getBlock('testBlock'); + } +} diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntitiesToIdsTransformerTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntitiesToIdsTransformerTest.php index a0dacb63599..7026425cdc4 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntitiesToIdsTransformerTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntitiesToIdsTransformerTest.php @@ -5,6 +5,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\AbstractQuery; @@ -349,7 +350,7 @@ public function testCreateFailsWhenCannotGetIdProperty() $classMetadata = $this->getMockClassMetadata(); $classMetadata->expects($this->once())->method('getSingleIdentifierFieldName') - ->will($this->throwException(new \Doctrine\ORM\Mapping\MappingException())); + ->will($this->throwException(new MappingException())); $em = $this->getMockEntityManager(); $em->expects($this->once())->method('getClassMetadata') @@ -408,6 +409,7 @@ private function createMockEntityList($property, array $values) foreach ($values as $value) { $result[] = $this->createMockEntity($property, $value); } + return $result; } @@ -423,6 +425,7 @@ private function createMockEntity($property, $value) $getter = 'get' . ucfirst($property); $result = $this->getMock('MockEntity', array($getter)); $result->expects($this->any())->method($getter)->will($this->returnValue($value)); + return $result; } diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntityToIdTransformerTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntityToIdTransformerTest.php index bc2ef2f02af..58a58a2cfe5 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntityToIdTransformerTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/EntityToIdTransformerTest.php @@ -112,6 +112,7 @@ public function testReverseTransformQueryBuilder() $qb->expects($self->once()) ->method('getQuery') ->will($self->returnValue($query)); + return $qb; }; @@ -204,7 +205,7 @@ function () { } /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException * @expectedExceptionMessage Expected argument of type "callable", "string" given */ public function testCallbackException() @@ -239,6 +240,7 @@ private function createMockEntity($property, $value) $getter = 'get' . ucfirst($property); $result = $this->getMock('MockEntity', array($getter)); $result->expects($this->any())->method($getter)->will($this->returnValue($value)); + return $result; } } diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Extension/DataBlockExtensionTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Extension/DataBlockExtensionTest.php new file mode 100644 index 00000000000..c379d73e9b5 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Extension/DataBlockExtensionTest.php @@ -0,0 +1,58 @@ + 1, 'subblock' => 1, 'block_config' => 1); + + public function setUp() + { + $this->manager = $this->getMockForAbstractClass('Oro\Bundle\UserBundle\Acl\ManagerInterface'); + $this->formExtension = new DataBlockExtension($this->manager); + } + + public function testSetDefaultOptions() + { + /** @var OptionsResolver $resolver */ + $resolver = new OptionsResolver(); + $this->formExtension->setDefaultOptions($resolver); + + $this->assertEquals( + $this->options, + $this->readAttribute($resolver, 'knownOptions') + ); + } + + public function testGetExtendedType() + { + $this->assertEquals('form', $this->formExtension->getExtendedType()); + } + + public function testBuildView() + { + /** @var FormView $formView */ + $formView = new FormView(); + + /** @var \Symfony\Component\Form\FormInterface $form */ + $form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + + $this->formExtension->buildView($formView, $form, $this->options); + + $this->assertEquals($this->options['block'], $formView->vars['block']); + $this->assertEquals($this->options['subblock'], $formView->vars['subblock']); + $this->assertEquals($this->options['block_config'], $formView->vars['block_config']); + } +} diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Twig/DataBlocksTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Twig/DataBlocksTest.php new file mode 100644 index 00000000000..590adbe5ac3 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Twig/DataBlocksTest.php @@ -0,0 +1,138 @@ + array( + 'title' => 'Second', + 'class' => null, + 'subblocks' => array( + 'text_3__subblock' => array( + 'code' => 'text_3__subblock', + 'title' => null, + 'data' => array(null), + ), + ), + ), + 'first' => array( + 'title' => 'First Block', + 'class' => null, + 'subblocks' => array( + 'first' => array( + 'code' => 'first', + 'title' => null, + 'data' => array(null), + ), + 'second' => array( + 'code' => 'second', + 'title' => 'Second SubBlock', + 'data' => array(null), + ), + ), + ), + 'third' => array( + 'title' => 'Third', + 'class' => null, + 'subblocks' => array( + 'text_4__subblock' => array( + 'code' => 'text_4__subblock', + 'title' => null, + 'data' => array(null), + ), + 'first' => array( + 'code' => 'first', + 'title' => null, + 'data' => array(null), + ), + ), + ), + ); + + public function setUp() + { + $this->dataBlocks = new DataBlocks(); + + $this->factory = Forms::createFormFactoryBuilder() + ->addTypeExtension(new DataBlockExtension()) + ->getFormFactory(); + + $this->twig = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + + $this->twig->expects($this->any()) + ->method('render') + ->will($this->returnValue(null)); + $this->twig->expects($this->any()) + ->method('getLoader') + ->will($this->returnValue($this->getMockForAbstractClass('\Twig_LoaderInterface'))); + } + + public function testConstruct() + { + $this->assertInstanceOf( + 'Symfony\Component\PropertyAccess\PropertyAccessor', + $this->readAttribute($this->dataBlocks, 'accessor') + ); + } + + public function testRender() + { + $options = array('block_config' => + array( + 'first' => array( + 'priority' => 1, + 'title' => 'First Block', + 'subblocks' => array( + 'first' => array(), + 'second' => array( + 'title' => 'Second SubBlock' + ), + ), + ), + 'second' => array( + 'priority' => 2, + ) + ) + ); + $builder = $this->factory->createNamedBuilder('test', 'form', null, $options); + $builder->add('text_1', null, array('block' => 'first', 'subblock' => 'second')); + $builder->add('text_2', null, array('block' => 'first')); + $builder->add('text_3', null, array('block' => 'second')); + $builder->add('text_4', null, array('block' => 'third')); + $builder->add('text_5', null, array('block' => 'third', 'subblock' => 'first')); + $builder->add('text_6', null); + + $formView = $builder->getForm()->createView(); + + $result = $this->dataBlocks->render($this->twig, array('form' => $formView), $formView); + + $this->assertEquals($this->testFormConfig, $result); + } +} diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/EntityIdentifierTypeTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/EntityIdentifierTypeTest.php index 18228add316..d53545f9714 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/EntityIdentifierTypeTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/EntityIdentifierTypeTest.php @@ -99,6 +99,7 @@ public function bindDataProvider() { $self = $this; $entitiesId1234 = $this->createMockEntityList('id', array(1, 2, 3, 4)); + return array( 'default' => array( '1,2,3,4', @@ -171,6 +172,7 @@ public function bindDataProvider() 'queryBuilder' => function ($repository, array $ids) use ($self) { $result = $repository->createQueryBuilder('o'); $result->where('o.id IN (:values)')->setParameter('values', $ids); + return $result; } ), @@ -190,8 +192,8 @@ public function bindDataProvider() /** * @dataProvider createErrorsDataProvider - * @param array $options - * @param array $expectedCalls + * @param array $options + * @param array $expectedCalls * @param string $expectedException * @param string $expectedExceptionMessage */ @@ -306,6 +308,7 @@ function ($transformer) use ($options) { 'queryBuilderCallback', $transformer ); + return true; } ) @@ -333,6 +336,7 @@ private function createMockEntityList($property, array $values) foreach ($values as $value) { $result[] = $this->createMockEntity($property, $value); } + return $result; } @@ -348,6 +352,7 @@ private function createMockEntity($property, $value) $getter = 'get' . ucfirst($property); $result = $this->getMock('MockEntity', array($getter)); $result->expects($this->any())->method($getter)->will($this->returnValue($value)); + return $result; } @@ -418,6 +423,7 @@ protected function getMockEntitiesToIdsTransformer() ->setMethods(array('transform', 'reverseTransform')) ->getMockForAbstractClass(); } + return $this->entitiesToIdsTransformer; } } diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroJquerySelect2HiddenTypeTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroJquerySelect2HiddenTypeTest.php index dcd2b624571..1e2014e7e06 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroJquerySelect2HiddenTypeTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroJquerySelect2HiddenTypeTest.php @@ -115,7 +115,6 @@ public function testBindData( } } - /** * Data provider for testBindData * @@ -125,6 +124,7 @@ public function testBindData( public function bindDataProvider() { $entityId1 = $this->createMockEntity('id', 1); + return array( 'use autocomplete_alias' => array( '1', @@ -165,7 +165,11 @@ public function bindDataProvider() 'extra_config' => 'autocomplete' ), 'attr' => array( - 'data-entity' => json_encode(array('id' => 1, 'bar' => 'Bar value', 'baz' => 'Baz value')) + 'data-entities' => json_encode( + array( + array('id' => 1, 'bar' => 'Bar value', 'baz' => 'Baz value') + ) + ) ) ) ), @@ -204,7 +208,11 @@ public function bindDataProvider() 'route_name' => 'custom_route' ), 'attr' => array( - 'data-entity' => json_encode(array('id' => 1, 'bar' => 'Bar value', 'baz' => 'Baz value')) + 'data-entities' => json_encode( + array( + array('id' => 1, 'bar' => 'Bar value', 'baz' => 'Baz value') + ) + ) ) ) ) @@ -213,8 +221,8 @@ public function bindDataProvider() /** * @dataProvider createErrorsDataProvider - * @param array $options - * @param array $expectedCalls + * @param array $options + * @param array $expectedCalls * @param string $expectedException * @param string $expectedExceptionMessage */ @@ -318,6 +326,7 @@ private function createMockEntity($property, $value) $getter = 'get' . ucfirst($property); $result = $this->getMock('MockEntity', array($getter)); $result->expects($this->any())->method($getter)->will($this->returnValue($value)); + return $result; } @@ -395,6 +404,7 @@ public function getMockEntityToIdTransformer() ->setMethods(array('transform', 'reverseTransform')) ->getMockForAbstractClass(); } + return $this->entityToIdTransformer; } } diff --git a/src/Oro/Bundle/FormBundle/Twig/FormExtension.php b/src/Oro/Bundle/FormBundle/Twig/FormExtension.php new file mode 100644 index 00000000000..0586f08de65 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Twig/FormExtension.php @@ -0,0 +1,27 @@ + true, 'needs_environment' => true) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_form'; + } +} diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php index 48c96b4fdc0..719d8045e1a 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php @@ -3,13 +3,10 @@ namespace Oro\Bundle\GridBundle\Filter\ORM\Flexible; use Doctrine\Common\Persistence\ObjectRepository; - use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; - use Oro\Bundle\FilterBundle\Form\Type\Filter\ChoiceFilterType; use Oro\Bundle\FlexibleEntityBundle\Entity\Attribute; use Oro\Bundle\FlexibleEntityBundle\Entity\AttributeOption; - use Oro\Bundle\GridBundle\Filter\ORM\ChoiceFilter; class FlexibleOptionsFilter extends AbstractFlexibleFilter diff --git a/src/Oro/Bundle/GridBundle/Property/TwigTemplateProperty.php b/src/Oro/Bundle/GridBundle/Property/TwigTemplateProperty.php index cc1a4ee4521..fc744bfcc5f 100644 --- a/src/Oro/Bundle/GridBundle/Property/TwigTemplateProperty.php +++ b/src/Oro/Bundle/GridBundle/Property/TwigTemplateProperty.php @@ -27,14 +27,43 @@ class TwigTemplateProperty extends AbstractProperty implements TwigPropertyInter */ protected $field; + /** + * @var array + */ + protected $context; + + /** + * @var array + */ + protected $reservedKeys = array( + 'field', + 'record', + 'value', + ); + + /** * @param FieldDescriptionInterface $field - * @param string $templateName + * @param string $templateName + * @param array $context + * @throws \InvalidArgumentException */ - public function __construct(FieldDescriptionInterface $field, $templateName) + public function __construct(FieldDescriptionInterface $field, $templateName, $context = array()) { $this->field = $field; $this->templateName = $templateName; + $this->context = $context; + + $checkInvalidArgument = array_intersect_key($context, array_flip($this->reservedKeys)); + if (count($checkInvalidArgument)) { + throw new \InvalidArgumentException( + sprintf( + 'Context of template "%s" includes reserved key(s) - (%s)', + $this->templateName, + implode(', ', array_keys($checkInvalidArgument)) + ) + ); + } } /** @@ -67,16 +96,18 @@ protected function getTemplate() /** * Render field template - * * @param ResultRecordInterface $record * @return string */ public function getValue(ResultRecordInterface $record) { - $context = array( - 'field' => $this->field, - 'record' => $record, - 'value' => $record->getValue($this->field->getFieldName()), + $context = array_merge( + $this->context, + array( + 'field' => $this->field, + 'record' => $record, + 'value' => $record->getValue($this->field->getFieldName()), + ) ); return $this->getTemplate()->render($context); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js index 765ee77cfc7..a975c39648b 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js @@ -35,7 +35,7 @@ Oro.Datagrid.Action.DeleteAction = Oro.Datagrid.Action.ModelAction.extend({ self.getErrorDialog().open(); }, success: function() { - var messageText = Translator.get('Item was deleted'); + var messageText = _.__('Item was deleted'); if (!_.isUndefined(Oro.NotificationFlashMessage)) { Oro.NotificationFlashMessage('success', messageText); } else { @@ -53,9 +53,9 @@ Oro.Datagrid.Action.DeleteAction = Oro.Datagrid.Action.ModelAction.extend({ getConfirmDialog: function() { if (!this.confirmModal) { this.confirmModal = new Oro.BootstrapModal({ - title: Translator.get('Delete Confirmation'), - content: Translator.get('Are you sure you want to delete this item?'), - okText: 'Yes, Delete', + title: _.__('Delete Confirmation'), + content: _.__('Are you sure you want to delete this item?'), + okText: _.__('Yes, Delete'), allowCancel: 'false' }); this.confirmModal.on('ok', _.bind(this.doDelete, this)); @@ -71,8 +71,8 @@ Oro.Datagrid.Action.DeleteAction = Oro.Datagrid.Action.ModelAction.extend({ getErrorDialog: function() { if (!this.errorModal) { this.confirmModal = new Oro.BootstrapModal({ - title: 'Delete Error', - content: 'Cannot delete item.', + title: _.__('Delete Error'), + content: _.__('Cannot delete item.'), cancelText: false }); } diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/booleancell.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/booleancell.js index 7846abb0057..35a7e939415 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/booleancell.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/booleancell.js @@ -21,7 +21,13 @@ Oro.Datagrid.Cell.BooleanCell = Backgrid.BooleanCell.extend({ listenRowClick: true, /** @property {Object} */ - editor: _.template(" <%= editable ? '' : 'disabled' %> />'"), + editor: _.template( + " <%= editable ? '' : 'disabled' %> />'" + ), + + /** @property {String} */ + dataIdentifier: null, /** * @inheritDoc @@ -29,6 +35,7 @@ Oro.Datagrid.Cell.BooleanCell = Backgrid.BooleanCell.extend({ initialize: function(options) { Backgrid.BooleanCell.prototype.initialize.apply(this, arguments); this.editable = this.column.get("editable"); + this.dataIdentifier = this._generateUniqueIdentifier(); }, /** @@ -37,8 +44,9 @@ Oro.Datagrid.Cell.BooleanCell = Backgrid.BooleanCell.extend({ render: function () { this.$el.empty(); this.currentEditor = $(this.editor({ - checked: this.formatter.fromRaw(this.model.get(this.column.get("name"))), - editable: this.editable + checked: this.formatter.fromRaw(this.model.get(this.column.get("name"))), + editable: this.editable, + dataIdentifier: this.dataIdentifier })); this.$el.append(this.currentEditor); return this; @@ -75,10 +83,19 @@ Oro.Datagrid.Cell.BooleanCell = Backgrid.BooleanCell.extend({ /** * @param {Backgrid.Row} row + * @param {Event} e */ - onRowClicked: function(row) { - if (this.editable) { + onRowClicked: function(row, e) { + if (this.editable && $(e.target).data('identifier') !== this.dataIdentifier) { this.currentEditor.click(); } + }, + + /** + * @return {String} + */ + _generateUniqueIdentifier: function() { + var randomString = Math.random().toString(36).slice(-8); + return 'checkbox_' + this.cid + '_' + randomString; } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/listener/columnformlistener.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/listener/columnformlistener.js index 4c507fb5766..d51d56591b7 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/listener/columnformlistener.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/listener/columnformlistener.js @@ -173,8 +173,8 @@ Oro.Datagrid.Listener.ColumnFormListener = Oro.Datagrid.Listener.AbstractListene */ _onExecuteRefreshAction: function(action, options) { this._confirmAction(action, options, 'refresh', { - title: 'Refresh Confirmation', - content: 'Your local changes will be lost. Are you sure you want to refresh grid?' + title: _.__('Refresh Confirmation'), + content: _.__('Your local changes will be lost. Are you sure you want to refresh grid?') }); }, @@ -187,8 +187,8 @@ Oro.Datagrid.Listener.ColumnFormListener = Oro.Datagrid.Listener.AbstractListene */ _onExecuteResetAction: function(action, options) { this._confirmAction(action, options, 'reset', { - title: 'Reset Confirmation', - content: 'Your local changes will be lost. Are you sure you want to reset grid?' + title: _.__('Reset Confirmation'), + content: _.__('Your local changes will be lost. Are you sure you want to reset grid?') }); }, @@ -233,8 +233,8 @@ Oro.Datagrid.Listener.ColumnFormListener = Oro.Datagrid.Listener.AbstractListene this.confirmModal = this.confirmModal || {}; if (!this.confirmModal[type]) { this.confirmModal[type] = new Oro.BootstrapModal(_.extend({ - title: 'Confirmation', - okText: 'Ok, got it.', + title: _.__('Confirmation'), + okText: _.__('Ok, got it.'), className: 'modal modal-primary', okButtonClass: 'btn-primary btn-large' }, options)); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js index 4e58d6f9653..8d051b3718c 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js @@ -10,7 +10,7 @@ Oro.Datagrid = Oro.Datagrid || {}; Oro.Datagrid.PageSize = Backbone.View.extend({ /** @property */ template: _.template( - '' + + '' + '
    ' + '' + - '' + - '' + + '' + + '' + + '' + '
    ' + '' + '' + diff --git a/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..6dd0fd2682c --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,20 @@ +"Item was deleted": "Item was deleted" +"Delete Confirmation": "Delete Confirmation" +"Are you sure you want to delete this item?": "Are you sure you want to delete this item?" +"Yes, Delete": "Yes, Delete" +"Delete Error": "Delete Error" +"Cannot delete item.": "Cannot delete item." +"Refresh Confirmation": "Refresh Confirmation" +"Your local changes will be lost. Are you sure you want to refresh grid?": "Your local changes will be lost. Are you sure you want to refresh grid?" +"Reset Confirmation": "Reset Confirmation" +"Your local changes will be lost. Are you sure you want to reset grid?": "Your local changes will be lost. Are you sure you want to reset grid?" +"Confirmation": "Confirmation" +"Ok, got it.": "Ok, got it." +"edit": "edit" +"copy": "copy" +"remove": "remove" +"Status": "Status" +"All": "All" +"only short": "only short" +"this is long text for test": "this is long text for test" +"View per page": "View per page" diff --git a/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.es.yml b/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.es.yml new file mode 100644 index 00000000000..7ae418db76e --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/translations/jsmessages.es.yml @@ -0,0 +1,20 @@ +"Item was deleted": "Artículo sido eliminada" +"Delete Confirmation": "Eliminar confirmación" +"Are you sure you want to delete this item?": "¿Está seguro que desea eliminar este artículo?" +"Yes, Delete": "Sí, eliminar" +"Delete Error": "Eliminar Error" +"Cannot delete item.": "No se puede eliminar el artículo." +"Refresh Confirmation": "Actualizar Confirmación" +"Your local changes will be lost. Are you sure you want to refresh grid?": "Se perderán los cambios locales. ¿Está seguro que desea actualizar la red?" +"Reset Confirmation": "Restablecer Confirmación" +"Your local changes will be lost. Are you sure you want to reset grid?": "Se perderán los cambios locales. ¿Está seguro de que desea restablecer la red?" +"Confirmation": "Confirmación" +"Ok, got it.": "Ok, lo tengo." +"edit": "editar" +"copy": "copiar" +"remove": "eliminar" +"Status": "Estado" +"All": "Todo" +"only short": "sólo a corto" +"this is long text for test": "Este es un texto largo para la prueba" +"View per page": "Ver por página" diff --git a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig index 47746d6187c..6a24d42d59f 100644 --- a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig +++ b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig @@ -4,8 +4,10 @@ {% set form = datagridView.formView %} - diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php index e752a698533..4c2600661e8 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Filter/ORM/Flexible/FlexibleOptionsFilterTest.php @@ -142,4 +142,4 @@ public function testGetRenderSettingsFailsWhenAttributeIsNotFound() $this->model->getRenderSettings(); } -} \ No newline at end of file +} diff --git a/src/Oro/Bundle/GridBundle/Tests/Unit/Property/TwigTemplatePropertyTest.php b/src/Oro/Bundle/GridBundle/Tests/Unit/Property/TwigTemplatePropertyTest.php index 60d75807911..f37b7123c91 100644 --- a/src/Oro/Bundle/GridBundle/Tests/Unit/Property/TwigTemplatePropertyTest.php +++ b/src/Oro/Bundle/GridBundle/Tests/Unit/Property/TwigTemplatePropertyTest.php @@ -7,11 +7,13 @@ class TwigTemplatePropertyTest extends \PHPUnit_Framework_TestCase { - const TEST_FIELD_NAME = 'test_field_name'; - const TEST_DB_FIELD_NAME = 'test_db_field_name'; - const TEST_FIELD_VALUE = 'test_field_value'; - const TEST_TEMPLATE_NAME = 'test_template_name'; - const TEST_RENDERED_TEMPLATE = 'test_rendered template'; + const TEST_FIELD_NAME = 'test_field_name'; + const TEST_DB_FIELD_NAME = 'test_db_field_name'; + const TEST_FIELD_VALUE = 'test_field_value'; + const TEST_TEMPLATE_NAME = 'test_template_name'; + const TEST_RENDERED_TEMPLATE = 'test_rendered template'; + const TEST_TEMPLATE_CONTEXT_KEY = 'test_template_context_key'; + const TEST_TEMPLATE_CONTEXT_VALUE = 'test_template_context_value'; /** * @var TwigTemplateProperty @@ -29,7 +31,13 @@ protected function setUp() $this->fieldDescription->setName(self::TEST_FIELD_NAME); $this->fieldDescription->setFieldName(self::TEST_DB_FIELD_NAME); - $this->property = new TwigTemplateProperty($this->fieldDescription, self::TEST_TEMPLATE_NAME); + $this->property = new TwigTemplateProperty( + $this->fieldDescription, + self::TEST_TEMPLATE_NAME, + array( + self::TEST_TEMPLATE_CONTEXT_KEY => self::TEST_TEMPLATE_CONTEXT_VALUE + ) + ); } protected function tearDown() @@ -43,6 +51,22 @@ public function testGetName() $this->assertEquals(self::TEST_FIELD_NAME, $this->property->getName()); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Context of template "test_template_name" includes reserved key(s) - (field, value) + */ + public function testInvalidContext() + { + $this->property = new TwigTemplateProperty( + $this->fieldDescription, + self::TEST_TEMPLATE_NAME, + array( + 'field' => 'field', + 'value' => 'value', + ) + ); + } + public function testSetEnvironment() { $environment = new \Twig_Environment(); @@ -76,10 +100,12 @@ public function testGetValue() true, array('render') ); + $expectedContext = array( - 'field' => $this->fieldDescription, - 'record' => $record, - 'value' => self::TEST_FIELD_VALUE, + 'field' => $this->fieldDescription, + 'record' => $record, + 'value' => self::TEST_FIELD_VALUE, + self::TEST_TEMPLATE_CONTEXT_KEY => self::TEST_TEMPLATE_CONTEXT_VALUE ); $template->expects($this->once()) ->method('render') diff --git a/src/Oro/Bundle/ImapBundle/.gitignore b/src/Oro/Bundle/ImapBundle/.gitignore new file mode 100644 index 00000000000..00ae1784e18 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/.gitignore @@ -0,0 +1,2 @@ +/PoC/* +*~ diff --git a/src/Oro/Bundle/ImapBundle/Connector/Exception/InvalidConfigurationException.php b/src/Oro/Bundle/ImapBundle/Connector/Exception/InvalidConfigurationException.php new file mode 100644 index 00000000000..fa33caeca05 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Exception/InvalidConfigurationException.php @@ -0,0 +1,7 @@ +host = $host; + $this->port = $port; + $this->ssl = $ssl; + $this->user = $user; + $this->password = $password; + } + + /** + * Gets the host name of IMAP server + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Sets the host name of IMAP server + * + * @param string $host + */ + public function setHost($host) + { + $this->host = $host; + } + + /** + * Gets the port of IMAP server + * + * @return string + */ + public function getPort() + { + return $this->port; + } + + /** + * Sets the port of IMAP server + * + * @param string $port + */ + public function setPort($port) + { + $this->port = $port; + } + + /** + * Gets the SSL type to be used to connect to IMAP server + * + * @return string + */ + public function getSsl() + { + return $this->ssl; + } + + /** + * Sets the SSL type to be used to connect to IMAP server + * + * @param string $ssl Can be empty string, 'ssl' or 'tsl' + */ + public function setSsl($ssl) + { + $this->ssl = $ssl; + } + + /** + * Gets the user name + * + * @return string + */ + public function getUser() + { + return $this->user; + } + + /** + * Sets the user name + * + * @param string $user + */ + public function setUser($user) + { + $this->user = $user; + } + + /** + * Gets the user password + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Sets the user password + * + * @param string $password + */ + public function setPassword($password) + { + $this->password = $password; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/ImapConnector.php b/src/Oro/Bundle/ImapBundle/Connector/ImapConnector.php new file mode 100644 index 00000000000..f08d1fa365b --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/ImapConnector.php @@ -0,0 +1,165 @@ +config = $config; + $this->factory = $factory; + $this->imap = null; + } + + /** + * Gets the search query builder + * + * @return SearchQueryBuilder + */ + public function getSearchQueryBuilder() + { + $this->ensureConnected(); + + return new SearchQueryBuilder(new SearchQuery($this->searchStringManager)); + } + + /** + * @param Folder|string|null $parentFolder + * @param SearchQuery|null $query + * @return Message[] + */ + public function findItems($parentFolder = null, $query = null) + { + $this->ensureConnected(); + + if ($parentFolder !== null) { + $this->imap->selectFolder($parentFolder); + } + + $searchString = ''; + if ($query !== null) { + $searchString = $query->convertToSearchString(); + } + if (empty($searchString)) { + // Return all messages + return iterator_to_array($this->imap); + } + + $ids = $this->imap->search(array($searchString)); + + $result = array(); + foreach ($ids as $messageId) { + $result[] = $this->imap->getMessage($messageId); + } + + return $result; + } + + /** + * Finds folders. + * + * @param string|null $parentFolder The global name of a parent folder. + * @param bool $recursive Determines whether + * @return Folder[] + */ + public function findFolders($parentFolder = null, $recursive = false) + { + $this->ensureConnected(); + + return $this->getSubFolders($this->imap->getFolders($parentFolder), $recursive); + } + + /** + * Finds a folder by its name. + * + * @param string $name The global name of the folder. + * @return Folder + */ + public function findFolder($name) + { + $this->ensureConnected(); + + return $this->imap->getFolders($name); + } + + /** + * Retrieves item detail by its id. + * + * @param int $uid The UID of a message + * @return Message + */ + public function getItem($uid) + { + $this->ensureConnected(); + + $id = $this->imap->getNumberByUniqueId($uid); + + return $this->imap->getMessage($id); + } + + /** + * Makes sure that there is active connection to IMAP server + */ + protected function ensureConnected() + { + if ($this->imap === null) { + $imapServices = $this->factory->createImapServices($this->config); + $this->imap = $imapServices->getStorage(); + $this->searchStringManager = $imapServices->getSearchStringManager(); + } + } + + /** + * Gets sub folders. + * + * @param Folder $parentFolder The parent folder. + * @param bool $recursive Determines whether + * @return Folder[] + */ + protected function getSubFolders(Folder $parentFolder, $recursive = false) + { + $result = array(); + foreach ($parentFolder as $folder) { + $result[] = $folder; + if ($recursive) { + $result = array_merge($result, $this->getSubFolders($folder, $recursive)); + } + } + + return $result; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/ImapConnectorFactory.php b/src/Oro/Bundle/ImapBundle/Connector/ImapConnectorFactory.php new file mode 100644 index 00000000000..475faea1970 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/ImapConnectorFactory.php @@ -0,0 +1,65 @@ +factory = $factory; + $this->connectorClass = $connectorClass; + } + + /** + * Creates the IMAP connector for the given user + * + * @param int $userId The user id + * @return ImapConnector + */ + public function createUserImapConnector($userId) + { + // TODO: Implement logic to get IMAP configuration for the given user + $host = ''; + $port = ''; + $ssl = ''; + $user = ''; + $password = ''; + + return $this->createImapConnector( + new ImapConfig($host, $port, $ssl, $user, $password) + ); + } + + /** + * Creates the IMAP connector based on the given configuration + * + * @param ImapConfig $config The configuration of IMAP service, such as host, port, user name and others + * @return ImapConnector + */ + public function createImapConnector(ImapConfig $config) + { + $connectorClass = $this->connectorClass; + + return new $connectorClass($config, $this->factory); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/ImapServices.php b/src/Oro/Bundle/ImapBundle/Connector/ImapServices.php new file mode 100644 index 00000000000..e357284d982 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/ImapServices.php @@ -0,0 +1,44 @@ +imapStorage = $imapStorage; + $this->searchStringManager = $searchStringManager; + } + + /** + * Gets the search string manager + * + * @return SearchStringManagerInterface + */ + public function getSearchStringManager() + { + return $this->searchStringManager; + } + + /** + * Gets the IMAP storage + * + * @return Imap + */ + public function getStorage() + { + return $this->imapStorage; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/ImapServicesFactory.php b/src/Oro/Bundle/ImapBundle/Connector/ImapServicesFactory.php new file mode 100644 index 00000000000..3c1e3eb49e1 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/ImapServicesFactory.php @@ -0,0 +1,117 @@ +defaultImapServices = $imapServicesMapping['']; + unset($imapServicesMapping['']); + $this->imapServicesMapping = $imapServicesMapping; + } + + /** + * @param ImapConfig $config + * @return ImapServices + */ + public function createImapServices(ImapConfig $config) + { + $defaultImapStorage = $this->getDefaultImapStorage($config); + + $foundItem = $this->findImapServicesConfig($defaultImapStorage->capability()); + + $imapStorageClass = + ($foundItem === null || strcmp($foundItem[0], get_class($defaultImapStorage)) === 0) + ? null + : $foundItem[0]; + $searchStringBuilderClass = + $foundItem === null + ? $this->defaultImapServices[1] + : $foundItem[1]; + + $imapStorage = $imapStorageClass === null + ? $defaultImapStorage + : new $imapStorageClass($defaultImapStorage); + + return new ImapServices( + $imapStorage, + new $searchStringBuilderClass() + ); + } + + /** + * @param ImapConfig $config + * @return Imap + */ + protected function getDefaultImapStorage(ImapConfig $config) + { + $params = array( + 'host' => $config->getHost(), + 'port' => $config->getPort(), + 'ssl' => $config->getSsl(), + 'user' => $config->getUser(), + 'password' => $config->getPassword() + ); + + $defaultImapStorageClass = $this->defaultImapServices[0]; + + return new $defaultImapStorageClass($params); + } + + /** + * Finds the configuration of IMAP services for the given IMAP server capabilities + * + * @param array $serverCapabilities + * @return array + */ + protected function findImapServicesConfig(array $serverCapabilities) + { + $result = null; + foreach ($this->imapServicesMapping as $key => $item) { + $filterResult = array_filter( + $serverCapabilities, + function ($capability) use ($key) { + return 0 === strcasecmp($capability, $key); + } + ); + if (!empty($filterResult)) { + $result = $item; + break; + } + } + + return $result; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchQueryBuilder.php b/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchQueryBuilder.php new file mode 100644 index 00000000000..e794879da84 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchQueryBuilder.php @@ -0,0 +1,109 @@ +query = $query; + } + + /** + * Adds AND operator. + * + * @param Closure $callback + * @return $this + */ + public function andOperator(Closure $callback = null) + { + $this->query->andOperator(); + if ($callback instanceof Closure) { + $this->processCallback($callback); + } + + return $this; + } + + /** + * Adds OR operator. + * + * @param Closure $callback + * @return $this + */ + public function orOperator(Closure $callback = null) + { + $this->query->orOperator(); + if ($callback instanceof Closure) { + $this->processCallback($callback); + } + + return $this; + } + + /** + * Adds OR operator. + * + * @param Closure $callback + * @return $this + */ + public function notOperator(Closure $callback = null) + { + $this->query->notOperator(); + if ($callback instanceof Closure) { + $this->processCallback($callback); + } + + return $this; + } + + /** + * Adds open parenthesis '('. + * + * @return $this + */ + public function openParenthesis() + { + $this->query->openParenthesis(); + + return $this; + } + + /** + * Adds close parenthesis ')'. + * + * @return $this + */ + public function closeParenthesis() + { + $this->query->closeParenthesis(); + + return $this; + } + + /** + * Returns the built SearchQuery object. + * + * @return SearchQuery + */ + public function get() + { + return $this->query; + } + + private function processCallback(Closure $callback) + { + $this->query->openParenthesis(); + call_user_func($callback, $this); + $this->query->closeParenthesis(); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchStringManager.php b/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchStringManager.php new file mode 100644 index 00000000000..23cd275c26b --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/AbstractSearchStringManager.php @@ -0,0 +1,150 @@ +getName() !== ')') { + $result .= ' '; + } + $normalizedOperator = $this->normalizeOperator($item->getName()); + if ($normalizedOperator !== '') { + $result .= $normalizedOperator; + $needWhitespace = ($item->getName() !== '('); + } else { + $needWhitespace = false; + } + } else { + if ($needWhitespace) { + $result .= ' '; + } + $result .= $item instanceof SearchQueryExpr + ? $this->processSubQueryValue($itemName, $item) + : $this->processItem($item, $itemName); + $needWhitespace = true; + } + } + + return $result; + } + + /** + * @param SearchQueryExprValueBase $item + * @param string|null $itemName + * @return string + * @throws \InvalidArgumentException + */ + protected function processItem(SearchQueryExprValueBase $item, $itemName = null) + { + if ($itemName === null && $item instanceof SearchQueryExprNamedItemInterface) { + $itemName = $item->getName(); + } + + $value = $item->getValue(); + + return $value instanceof SearchQueryExpr + ? $this->processSubQueryValue($itemName, $value) + : $this->processSimpleValue($itemName, $value, $item->getMatch()); + } + + /** + * @param string $itemName The property name + * @param SearchQueryExpr $value The sub query + * @return string + */ + protected function processSubQueryValue($itemName, SearchQueryExpr $value) + { + if ($value->isEmpty()) { + return ''; + } + + $result = $this->processExpr($value, $itemName); + if ($value->isComplex()) { + $result = sprintf('(%s)', $result); + } + + return $result; + } + + /** + * @param string $itemName The property name + * @param mixed $itemValue A constant the property value is compared + * @param int $match The match type. One of SearchQueryMatch::* values + * @return string + * @throws \InvalidArgumentException + */ + protected function processSimpleValue($itemName, $itemValue, $match) + { + if ($itemName === null) { + return $this->normalizeValue($itemValue, $match); + } + + $keyword = $this->getKeyword($itemName); + if (!$keyword) { + throw new \InvalidArgumentException(sprintf('Unsupported property "%s".', $itemName)); + } + + return sprintf( + '%s%s%s', + $keyword, + $this->getNameValueDelimiter(), + $this->normalizeValue($itemValue, $match) + ); + } + + /** + * @param string $operator A string value contains an operator name to be normalized + * @return string + */ + protected function normalizeOperator($operator) + { + if ($operator === 'AND') { + return ''; + } + + return $operator; + } + + /** + * @param mixed $value The value to be normalized + * @param int $match The match type. One of SearchQueryMatch::* values + * @return string + */ + protected function normalizeValue($value, $match) + { + if (is_string($value) && strpos($value, ' ')) { + return sprintf('"%s"', $value); + } + + return $value; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/GmailSearchStringManager.php b/src/Oro/Bundle/ImapBundle/Connector/Search/GmailSearchStringManager.php new file mode 100644 index 00000000000..d672837a8a9 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/GmailSearchStringManager.php @@ -0,0 +1,129 @@ + 'from', + 'to' => 'to', + 'cc' => 'cc', + 'bcc' => 'bcc', + 'participants' => array( + 'to', + 'cc', + 'bcc' + ), + 'subject' => 'subject', + 'body' => 'body', + 'attachment' => 'filename', + 'sent:before' => 'before', + 'sent:after' => 'after', + 'received:before' => 'before', + 'received:after' => 'after' + ); + + /** + * {@inheritdoc} + */ + protected function getNameValueDelimiter() + { + return ':'; + } + + /** + * {@inheritdoc} + */ + protected function getKeyword($itemName) + { + if (!isset(static::$keywords[$itemName])) { + return false; + } + + return static::$keywords[$itemName]; + } + + /** + * {@inheritdoc} + */ + public function isAcceptableItem($name, $value, $match) + { + if (!isset(static::$keywords[$name])) { + return false; + } + + return + ($match === SearchQueryMatch::DEFAULT_MATCH) + || ($match === SearchQueryMatch::SUBSTRING_MATCH) + || ($match === SearchQueryMatch::EXACT_MATCH); + } + + /** + * {@inheritdoc} + */ + public function buildSearchString(SearchQueryExpr $searchQueryExpr) + { + return sprintf('"%s"', $this->processExpr($searchQueryExpr)); + } + + /** + * {@inheritdoc} + */ + protected function processSubQueryValue($itemName, SearchQueryExpr $value) + { + if ($value->isEmpty()) { + return ''; + } + + if ($itemName !== null) { + $keyword = $this->getKeyword($itemName); + if (!$keyword) { + throw new \InvalidArgumentException(sprintf('Unsupported property "%s".', $itemName)); + } + $result = sprintf('%s%s', $keyword, $this->getNameValueDelimiter()); + } else { + $result = ''; + } + + + $expr = $this->processExpr($value); + $result .= $value->isComplex() + ? sprintf('(%s)', $expr) + : $expr; + + return $result; + } + + /** + * {@inheritdoc} + */ + protected function normalizeOperator($operator) + { + if ($operator === 'NOT') { + return '-'; + } + + return parent::normalizeOperator($operator); + } + + /** + * {@inheritdoc} + */ + protected function normalizeValue($value, $match) + { + $result = parent::normalizeValue($value, $match); + if ($match === SearchQueryMatch::EXACT_MATCH) { + $result = '+' . $result; + } + + return str_replace('"', '\\"', $result); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQuery.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQuery.php new file mode 100644 index 00000000000..bdcb7148842 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQuery.php @@ -0,0 +1,187 @@ +searchStringManager = $searchStringManager; + $this->expr = new SearchQueryExpr(); + } + + /** + * Creates new empty instance of this class. + * + * @return SearchQuery + */ + public function newInstance() + { + $calledClass = get_called_class(); + + return new $calledClass($this->searchStringManager); + } + + /** + * Gets the expression represents the search query. + * + * @return SearchQueryExpr + */ + public function getExpression() + { + return $this->expr; + } + + /** + * Adds a word phrase to be searched in all properties. + * + * @param string|SearchQuery $value The word phrase + * @param int $match The match type. One of SearchQueryMatch::* values + * @throws \InvalidArgumentException + */ + public function value($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + if ($value instanceof SearchQuery && $value->isComplex() && $match != SearchQueryMatch::DEFAULT_MATCH) { + throw new \InvalidArgumentException('The match argument can be specified only if the value argument is a string or a simple query.'); + } + + $this->andOperatorIfNeeded(); + $expr = $value instanceof SearchQuery + ? $value->getExpression() + : new SearchQueryExprValue($value, $match); + $this->expr->add($expr); + } + + /** + * Adds name/value pair specifying a word phrase and property where it need to be searched. + * + * @param string $name The property name + * @param string|SearchQuery $value The word phrase + * @param int $match The match type. One of SearchQueryMatch::* values + * @throws \InvalidArgumentException + */ + public function item($name, $value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + if ($value instanceof SearchQuery && $value->isComplex() && $match != SearchQueryMatch::DEFAULT_MATCH) { + throw new \InvalidArgumentException('The match argument can be specified only if the value argument is a string or a simple query.'); + } + if (!$this->searchStringManager->isAcceptableItem($name, $value, $match)) { + throw new \InvalidArgumentException(sprintf( + 'This combination of arguments are not valid. Name: %s. Value: %s. Match: %d.', + $name, + is_object($value) ? get_class($value) : $value, + $match + )); + } + + $this->andOperatorIfNeeded(); + $value = $value instanceof SearchQuery + ? $value->getExpression() + : $value; + $this->expr->add(new SearchQueryExprItem($name, $value, $match)); + } + + /** + * Adds AND operator. + */ + public function andOperator() + { + $this->expr->add(new SearchQueryExprOperator('AND')); + } + + /** + * Adds OR operator. + */ + public function orOperator() + { + $this->expr->add(new SearchQueryExprOperator('OR')); + } + + /** + * Adds NOT operator. + */ + public function notOperator() + { + $this->andOperatorIfNeeded(); + $this->expr->add(new SearchQueryExprOperator('NOT')); + } + + /** + * Adds open parenthesis '('. + */ + public function openParenthesis() + { + $this->andOperatorIfNeeded(); + $this->expr->add(new SearchQueryExprOperator('(')); + } + + /** + * Adds close parenthesis ')'. + */ + public function closeParenthesis() + { + $this->expr->add(new SearchQueryExprOperator(')')); + } + + /** + * Builds a string representation of the search query. + * + * @return string + * @throws \LogicException + */ + public function convertToSearchString() + { + return $this->searchStringManager->buildSearchString($this->expr); + } + + /** + * Checks if this query has no any expressions. + * + * @return bool + */ + public function isEmpty() + { + return $this->expr->isEmpty(); + } + + /** + * Checks if this query has more than one expression. + * + * @return bool + */ + public function isComplex() + { + return $this->expr->isComplex(); + } + + private function andOperatorIfNeeded() + { + $exprItems = $this->expr->getItems(); + $lastIndex = count($exprItems) - 1; + if ($lastIndex != -1) { + $lastItem = $exprItems[$lastIndex]; + if (!($lastItem instanceof SearchQueryExprOperator) + || (($lastItem instanceof SearchQueryExprOperator) && $lastItem->getName() == ')') + ) { + $this->andOperator(); + } + } + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryBuilder.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryBuilder.php new file mode 100644 index 00000000000..911162ee265 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryBuilder.php @@ -0,0 +1,168 @@ +processField('from', $value, $match); + return $this; + } + + /** + * Search by TO field. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function to($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('to', $value, $match); + return $this; + } + + /** + * Search by CC field. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function cc($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('cc', $value, $match); + return $this; + } + + /** + * Search by BCC field. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function bcc($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('bcc', $value, $match); + return $this; + } + + /** + * Search by TO, CC, or BCC fields. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function participants($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('participants', $value, $match); + return $this; + } + + /** + * Search by SUBJECT field. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function subject($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('subject', $value, $match); + return $this; + } + + /** + * Search by BODY field. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function body($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('body', $value, $match); + return $this; + } + + /** + * Search by the attachment file name. + * + * @param string|Closure $value + * @param int $match The match type. One of SearchQueryMatch::* values + * @return $this + */ + public function attachment($value, $match = SearchQueryMatch::DEFAULT_MATCH) + { + $this->processField('attachment', $value, $match); + return $this; + } + + /** + * Search by SENT field. + * + * @param string $fromValue + * @param string $toValue + * @return $this + */ + public function sent($fromValue = null, $toValue = null) + { + $this->processDateField('sent', $fromValue, $toValue); + return $this; + } + + /** + * Search by RECEIVED field. + * + * @param string $fromValue + * @param string $toValue + * @return $this + */ + public function received($fromValue = null, $toValue = null) + { + $this->processDateField('received', $fromValue, $toValue); + return $this; + } + + private function processDateField($name, $fromValue = null, $toValue = null) + { + if ($fromValue !== null) { + $this->query->item($name . ':after', $fromValue); + } + if ($toValue !== null) { + $this->query->item($name . ':before', $toValue); + } + } + + private function processField($name, $value, $match) + { + if ($value instanceof Closure) { + $exprBuilder = new SearchQueryValueBuilder($this->query->newInstance()); + call_user_func($value, $exprBuilder); + $value = $exprBuilder->get(); + } + $this->query->item($name, $value, $match); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExpr.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExpr.php new file mode 100644 index 00000000000..a037d51ca73 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExpr.php @@ -0,0 +1,181 @@ +position = 0; + } + + /** + * @param SearchQueryExprInterface $item + */ + public function add(SearchQueryExprInterface $item) + { + $this->items[] = $item; + } + + /** + * @param SearchQueryExprInterface[] $items + */ + public function setItems(array $items) + { + $this->items = $items; + } + + /** + * @return SearchQueryExprInterface[] + */ + public function getItems() + { + return $this->items; + } + + /** + * Checks if this object has no any expressions. + * + * @return bool + */ + public function isEmpty() + { + if (empty($this->items)) { + return true; + } + $isEmpty = true; + foreach ($this->items as $item) { + if ($item instanceof SearchQueryExprValueBase) { + $value = $item->getValue(); + $isEmpty = ($value instanceof SearchQueryExpr) + ? $value->isEmpty() + : false; + } elseif ($item instanceof SearchQueryExpr) { + $isEmpty = $item->isEmpty(); + } else { + $isEmpty = false; + } + if (!$isEmpty) { + break; + } + } + + return $isEmpty; + } + + /** + * Checks if this object has more than one expression. + * + * @return bool + */ + public function isComplex() + { + if (empty($this->items)) { + return false; + } + if (count($this->items) > 1) { + return true; + } + $isComplex = false; + $item = $this->items[0]; + if ($item instanceof SearchQueryExprValueBase) { + $value = $item->getValue(); + if ($value instanceof SearchQueryExpr) { + $isComplex = $value->isComplex(); + } + } elseif ($item instanceof SearchQueryExpr) { + $isComplex = $item->isComplex(); + } + + return $isComplex; + } + + /** + * {@inheritdoc} + */ + public function current() + { + return $this->items[$this->position]; + } + + /** + * {@inheritdoc} + */ + public function next() + { + ++$this->position; + } + + /** + * {@inheritdoc} + */ + public function key() + { + return $this->position; + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return isset($this->items[$this->position]); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->position = 0; + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return isset($this->items[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprInterface.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprInterface.php new file mode 100644 index 00000000000..c48b9279d4d --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprInterface.php @@ -0,0 +1,7 @@ +name = $name; + } + + /** + * The name of a property + * + * @var string + */ + private $name; + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprNamedItemInterface.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprNamedItemInterface.php new file mode 100644 index 00000000000..00decf38491 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprNamedItemInterface.php @@ -0,0 +1,16 @@ +name = $name; + } + + /** + * Can be one of 'AND', 'OR', 'NOT', '(', ')' + * + * @var + */ + private $name; + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValue.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValue.php new file mode 100644 index 00000000000..b04824a43a4 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValue.php @@ -0,0 +1,7 @@ +value = $value; + $this->match = $match; + } + + /** + * A word phrase or instance of SearchQueryExpr class + * + * @var string|SearchQueryExpr + */ + private $value; + + /** + * A match type. One of SearchQueryMatch::* values + * + * @var int + */ + private $match; + + /** + * @return string|SearchQueryExpr + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string|SearchQueryExpr $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return int + * @see SearchQueryMatch + */ + public function getMatch() + { + return $this->match; + } + + /** + * @param int $match One of SearchQueryMatch::* values + * @see SearchQueryMatch + */ + public function setMatch($match) + { + $this->match = $match; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValueInterface.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValueInterface.php new file mode 100644 index 00000000000..7043b63d1e3 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchQueryExprValueInterface.php @@ -0,0 +1,7 @@ +query->value($value); + + return $this; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManager.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManager.php new file mode 100644 index 00000000000..050dd2f0b03 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManager.php @@ -0,0 +1,196 @@ + 'FROM', + 'to' => 'TO', + 'cc' => 'CC', + 'bcc' => 'BCC', + 'participants' => array( + 'TO', + 'CC', + 'BCC' + ), + 'subject' => 'SUBJECT', + 'body' => 'BODY', + 'sent:before' => 'SENTBEFORE', + 'sent:after' => 'SENTSINCE', + 'received:before' => 'BEFORE', + 'received:after' => 'SINCE' + ); + + /** + * {@inheritdoc} + */ + protected function getNameValueDelimiter() + { + return ' '; + } + + /** + * {@inheritdoc} + */ + protected function getKeyword($itemName) + { + if (!isset(static::$keywords[$itemName])) { + return false; + } + + return static::$keywords[$itemName]; + } + + /** + * {@inheritdoc} + */ + public function isAcceptableItem($name, $value, $match) + { + if (!isset(static::$keywords[$name])) { + return false; + } + + return + ($match === SearchQueryMatch::DEFAULT_MATCH) + || ($match === SearchQueryMatch::SUBSTRING_MATCH); + } + + /** + * {@inheritdoc} + */ + public function buildSearchString(SearchQueryExpr $searchQueryExpr) + { + return $this->processExpr($this->arrangeExpr($searchQueryExpr)); + } + + /** + * Arranges the given search expression before it can be converted to its string representation + * As IMAP SEARCH command uses prefixed OR operator in its search expressions we need to prepare + * our search expression before it will be converted to a string. + * Examples: + * 'val1 OR val2' need to be changed to 'OR val1 val2' + * 'val1 OR val2 OR val3' need to be changed to 'OR val1 OR val2 val3' + * 'NOT val1 OR val2' need to be changed to 'OR NOT val1 val2' + * + * @param SearchQueryExpr $expr The search expression + * @return SearchQueryExpr + */ + protected function arrangeExpr(SearchQueryExpr $expr) + { + // Make a clone of the expression and find out OR operators + $result = new SearchQueryExpr; + $orOperatorPositions = array(); + $i = 0; + foreach ($expr as $item) { + if ($item instanceof SearchQueryExprOperator) { + $result->add($item); + if ($item->getName() === 'OR') { + $orOperatorPositions[] = $i; + } + } elseif ($item instanceof SearchQueryExpr) { + $result->add($this->arrangeExpr($item)); + } else { + /** @var SearchQueryExprValueBase $item */ + $value = $item->getValue(); + if ($value instanceof SearchQueryExpr) { + $item->setValue($this->arrangeExpr($value)); + } + $result->add($item); + } + $i++; + } + + // Arrange OR operators is any + foreach ($orOperatorPositions as $orPos) { + $i = $orPos - 1; + $parenthesisCounter = 0; + while ($i >= 0) { + $item = $result[$i]; + if ($item instanceof SearchQueryExprOperator) { + /** @var SearchQueryExprOperator $item */ + switch ($item->getName()) { + case '(': + $parenthesisCounter--; + break; + case ')': + $parenthesisCounter++; + break; + } + } elseif ($parenthesisCounter === 0) { + $this->moveOperator( + $result, + $orPos, + $this->correctNewPositionOfOperatorIfNeeded($result, $i) + ); + break; + } + $i--; + } + if ($i === -1 && $parenthesisCounter === 0) { + $this->moveOperator($result, $orPos, 0); + } + } + + return $result; + } + + /** + * Moves an operator in a search expression + * + * @param SearchQueryExpr $expr The search expression + * @param int $current The current position of the operator + * @param int $new The position where the operator to be moved + * @throws \InvalidArgumentException + */ + protected function moveOperator(SearchQueryExpr $expr, $current, $new) + { + if ($new < 0) { + throw new \InvalidArgumentException('The new position of the operator must be greater than or equal zero.'); + } + if ($current < $new) { + throw new \InvalidArgumentException('The current position of the operator must be greater than its new position.'); + } + + $operator = $expr[$current]; + $i = $current; + while ($i > $new) { + $expr[$i] = $expr[$i - 1]; + $i--; + } + $expr[$new] = $operator; + } + + /** + * Corrects new operator position if there are NOT operators before first OR operand + * + * @param SearchQueryExpr $expr + * @param int $pos The position of the first operand + * @return int + */ + protected function correctNewPositionOfOperatorIfNeeded(SearchQueryExpr $expr, $pos) + { + $i = $pos - 1; + while ($i >= 0) { + $item = $expr[$i]; + if ($item instanceof SearchQueryExprOperator) { + if ($item->getName() !== 'NOT') { + break; + } + } else { + break; + } + $i--; + } + + return $i + 1; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManagerInterface.php b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManagerInterface.php new file mode 100644 index 00000000000..047ee6501a1 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Connector/Search/SearchStringManagerInterface.php @@ -0,0 +1,29 @@ +root('oro_imap'); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/ImapBundle/DependencyInjection/OroImapExtension.php b/src/Oro/Bundle/ImapBundle/DependencyInjection/OroImapExtension.php new file mode 100644 index 00000000000..61f09c0750a --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/DependencyInjection/OroImapExtension.php @@ -0,0 +1,25 @@ +load('services.yml'); + } +} diff --git a/src/Oro/Bundle/ImapBundle/LICENSE b/src/Oro/Bundle/ImapBundle/LICENSE new file mode 100644 index 00000000000..938870a7a30 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Oro, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Attachment.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Attachment.php new file mode 100644 index 00000000000..f9f4412709d --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Attachment.php @@ -0,0 +1,104 @@ +part = $part; + } + + /** + * Gets the headers collection + * + * @return Headers + */ + public function getHeaders() + { + return $this->part->getHeaders(); + } + + /** + * Gets a header in specified format + * + * @param string $name The name of header, matches case-insensitive, but camel-case is replaced with dashes + * @param string $format change The type of return value to 'string' or 'array' + * @return Headers + */ + public function getHeader($name, $format = null) + { + return $this->part->getHeader($name, $format); + } + + /** + * Gets the attached file name + * + * @return Value + */ + public function getFileName() + { + if ($this->part->getHeaders()->has('Content-Disposition')) { + $contentDisposition = $this->part->getHeader('Content-Disposition'); + $value = Decode::splitContentType($contentDisposition->getFieldValue(), 'filename'); + $encoding = $contentDisposition->getEncoding(); + } else { + /** @var \Zend\Mail\Header\ContentType $contentType */ + $contentType = $this->part->getHeader('Content-Type'); + $value = $contentType->getParameter('name'); + $encoding = $contentType->getEncoding(); + } + + return new Value($value, $encoding); + } + + /** + * Gets the attachment content + * + * @return Content + */ + public function getContent() + { + if ($this->part->getHeaders()->has('Content-Type')) { + /** @var \Zend\Mail\Header\ContentType $contentTypeHeader */ + $contentTypeHeader = $this->part->getHeader('Content-Type'); + $contentType = $contentTypeHeader->getType(); + $charset = $contentTypeHeader->getParameter('charset'); + $encoding = $charset !== null ? $charset : 'ASCII'; + } else { + $contentType = 'text/plain'; + $encoding = 'ASCII'; + } + + if ($this->part->getHeaders()->has('Content-Transfer-Encoding')) { + $contentTransferEncoding = $this->part->getHeader('Content-Transfer-Encoding')->getFieldValue(); + switch (strtolower($contentTransferEncoding)) { + case 'base64': + $content = base64_decode($this->part->getContent()); + break; + case 'quoted-printable': + $content = quoted_printable_decode($this->part->getContent()); + break; + default: + $content = $this->part->getContent(); + break; + } + } else { + $content = $this->part->getContent(); + } + + return new Content($content, $contentType, $encoding); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Body.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Body.php new file mode 100644 index 00000000000..96e539f288b --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Body.php @@ -0,0 +1,132 @@ +part = $part; + } + + /** + * Gets the headers collection + * + * @return Headers + */ + public function getHeaders() + { + return $this->part->getHeaders(); + } + + /** + * Gets a header in specified format + * + * @param string $name The name of header, matches case-insensitive, but camel-case is replaced with dashes + * @param string $format change The type of return value to 'string' or 'array' + * @return Headers + */ + public function getHeader($name, $format = null) + { + return $this->part->getHeader($name, $format); + } + + /** + * Gets a string contains the message body content + * + * @param bool $format The required format of the body. Can be FORMAT_TEXT or FORMAT_HTML + * @return Content + * @throws Exception\InvalidBodyFormatException + */ + public function getContent($format = Body::FORMAT_TEXT) + { + if (!$this->part->isMultipart()) { + if ($format === Body::FORMAT_TEXT) { + return $this->extractContent($this->part); + } + } else { + $i = 0; + foreach ($this->part as $part) { + $contentTypeHeader = $this->getPartContentType($part); + if ($contentTypeHeader !== null) { + if ($format === Body::FORMAT_TEXT && $contentTypeHeader->getType() === 'text/plain') { + return $this->extractContent($part); + } elseif ($format === Body::FORMAT_HTML && $contentTypeHeader->getType() === 'text/html') { + return $this->extractContent($part); + } + } + } + } + + throw new Exception\InvalidBodyFormatException(sprintf( + 'A messages does not have %s content.', + $format === Body::FORMAT_TEXT ? 'TEXT' : 'HTML' + )); + } + + /** + * Extracts body content from the given part + * + * @param Part $part The message part where the content is stored + * @return Content + */ + protected function extractContent($part) + { + /** @var \Zend\Mail\Header\ContentType $contentTypeHeader */ + $contentTypeHeader = $this->getPartContentType($part); + if ($contentTypeHeader !== null) { + $contentType = $contentTypeHeader->getType(); + $charset = $contentTypeHeader->getParameter('charset'); + $encoding = $charset !== null ? $charset : 'ASCII'; + } else { + $contentType = 'text/plain'; + $encoding = 'ASCII'; + } + + if ($part->getHeaders()->has('Content-Transfer-Encoding')) { + $contentTransferEncoding = $part->getHeader('Content-Transfer-Encoding')->getFieldValue(); + switch (strtolower($contentTransferEncoding)) { + case 'base64': + $content = base64_decode($part->getContent()); + break; + case 'quoted-printable': + $content = quoted_printable_decode($part->getContent()); + break; + default: + $content = $part->getContent(); + break; + } + } else { + $content = $part->getContent(); + } + + return new Content($content, $contentType, $encoding); + } + + /** + * Gets the Content-Type for the given part + * + * @param Part $part The message part + * @return \Zend\Mail\Header\ContentType|null + */ + protected function getPartContentType($part) + { + return $part->getHeaders()->has('Content-Type') + ? $part->getHeader('Content-Type') + : null; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Content.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Content.php new file mode 100644 index 00000000000..669d228554a --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Content.php @@ -0,0 +1,59 @@ +content = $content; + $this->contentType = $contentType; + $this->encoding = $encoding; + } + + /** + * Gets the content data + * + * @return string|mixed + * @throws \Zend\Mail\Storage\Exception\RuntimeException + */ + public function getContent() + { + return $this->content; + } + + /** + * Gets the content type + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Gets the encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Exception/InvalidBodyFormatException.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Exception/InvalidBodyFormatException.php new file mode 100644 index 00000000000..ff8f6c2446a --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Exception/InvalidBodyFormatException.php @@ -0,0 +1,7 @@ +getMessageItems, self::X_GM_MSGID, self::X_GM_THRID, self::X_GM_LABELS); + + } + + /** + * {@inheritdoc} + */ + public function search(array $criteria) + { + if (!empty($criteria)) { + $lastItem = end($criteria); + if (strpos($lastItem, '"') === 0 && substr($lastItem, -strlen('"')) === '"') { + array_unshift($criteria, 'X-GM-RAW'); + } + } + + return parent::search($criteria); + } + + /** + * {@inheritdoc} + */ + protected function setExtHeaders(&$headers, array $data) + { + parent::setExtHeaders($headers, $data); + + $headers->addHeaderLine(self::X_GM_MSGID, $data[self::X_GM_MSGID]); + $headers->addHeaderLine(self::X_GM_THRID, $data[self::X_GM_THRID]); + $headers->addHeaderLine( + self::X_GM_LABELS, + isset($data[self::X_GM_LABELS]) ? $data[self::X_GM_LABELS] : array() + ); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php new file mode 100644 index 00000000000..83400bda572 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php @@ -0,0 +1,159 @@ +ignoreCloseCommand = true; + $this->currentFolder = $params->currentFolder; + $params = $params->protocol; + } + + parent::__construct($params); + $this->messageClass = 'Oro\Bundle\ImapBundle\Mail\Storage\Message'; + $this->getMessageItems = array(self::FLAGS, self::RFC822_HEADER, self::UID); + } + + /** + * Get capabilities from IMAP server + * + * @return string[] list of capabilities + */ + public function capability() + { + return $this->protocol->capability(); + } + + /** + * Gets UIDVALIDITY of currently selected folder + * + * @return int + */ + public function getUidValidity() + { + return $this->uidValidity; + } + + /** + * {@inheritdoc} + */ + public function getMessage($id) + { + $data = $this->protocol->fetch($this->getMessageItems, $id); + $header = $data[self::RFC822_HEADER]; + + $flags = array(); + foreach ($data[self::FLAGS] as $flag) { + $flags[] = isset(static::$knownFlags[$flag]) ? static::$knownFlags[$flag] : $flag; + } + + /** @var \Zend\Mail\Storage\Message $message */ + $message = new $this->messageClass(array( + 'handler' => $this, + 'id' => $id, + 'headers' => $header, + 'flags' => $flags + )); + + $headers = $message->getHeaders(); + $this->setExtHeaders($headers, $data); + + return $message; + } + + /** + * Searches messages by the given criteria + * + * @param array $criteria The search criteria + * @return string[] Message ids + * @throws \Zend\Mail\Storage\Exception\RuntimeException + */ + public function search(array $criteria) + { + if (empty($criteria)) { + throw new \Zend\Mail\Storage\Exception\RuntimeException('The search criteria must not be empty.'); + } + + $response = $this->protocol->search($criteria); + if (!is_array($response)) { + throw new \Zend\Mail\Storage\Exception\RuntimeException('Cannot search messages.'); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function selectFolder($globalName) + { + if ((string)$this->currentFolder === (string)$globalName) { + // The given folder already selected + return; + } + + $this->currentFolder = $globalName; + $selectResponse = $this->protocol->select($this->currentFolder); + if (!$selectResponse) { + $this->currentFolder = ''; + throw new \Zend\Mail\Storage\Exception\RuntimeException('cannot change folder, maybe it does not exist'); + } + + $this->uidValidity = $selectResponse['uidvalidity']; + } + + /** + * {@inheritdoc} + */ + public function close() + { + if ($this->ignoreCloseCommand) { + return; + } + + parent::close(); + } + + /** + * Sets additional message headers + * + * @param \Zend\Mail\Headers $headers + * @param array $data + */ + protected function setExtHeaders(&$headers, array $data) + { + $headers->addHeaderLine('UID', $data['UID']); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Message.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Message.php new file mode 100644 index 00000000000..71c17acef05 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Message.php @@ -0,0 +1,104 @@ +isMultipart()) { + return new Body($this); + } + + foreach ($this as $part) { + $contentType = $this->getPartContentType($part); + if ($contentType !== null) { + if ($contentType->getParameter('name') === null) { + return new Body($part); + } + } + } + + throw new \Zend\Mail\Storage\Exception\RuntimeException('Cannot find a message body.'); + } + + /** + * Gets the message attachments + * + * @return Attachment[] + */ + public function getAttachments() + { + if (!$this->isMultipart()) { + return array(); + } + + $result = array(); + foreach ($this as $part) { + /** @var Part $part */ + $contentType = $this->getPartContentType($part); + if ($contentType !== null) { + $name = $contentType->getParameter('name'); + if ($name !== null) { + $contentDisposition = $this->getPartContentDisposition($part); + if ($contentDisposition !== null) { + if (null !== Decode::splitContentType('attachment')) { + $result[] = new Attachment($part); + } + } else { + // The Content-Disposition may be missed, because it is introduced only in RFC 2183 + // In this case it is assumed that any part which has ;name= in the Content-Type is an attachment + $result[] = new Attachment($part); + } + } + } + } + + return $result; + } + + /** + * Gets the Content-Type for the given part + * + * @param Part $part The message part + * @return \Zend\Mail\Header\ContentType|null + */ + protected function getPartContentType($part) + { + return $part->getHeaders()->has('Content-Type') + ? $part->getHeader('Content-Type') + : null; + } + + /** + * Gets the Content-Disposition for the given part + * + * @param Part $part The message part + * @param bool $format Can be FORMAT_RAW or FORMAT_ENCODED, see HeaderInterface::FORMAT_* constants + * @return string|null + */ + protected function getPartContentDisposition($part, $format = HeaderInterface::FORMAT_RAW) + { + return $part->getHeaders()->has('Content-Disposition') + ? $part->getHeader('Content-Disposition')->getFieldValue($format) + : null; + } +} diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Value.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Value.php new file mode 100644 index 00000000000..98c00e3eccd --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Value.php @@ -0,0 +1,46 @@ +value = $value; + $this->encoding = $encoding; + } + + /** + * Gets the value + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Gets the value encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } +} diff --git a/src/Oro/Bundle/ImapBundle/OroImapBundle.php b/src/Oro/Bundle/ImapBundle/OroImapBundle.php new file mode 100644 index 00000000000..69b3d84b795 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/OroImapBundle.php @@ -0,0 +1,9 @@ +get('oro_imap.connector.factory'); + + // Creating IMAP connector for the ORO user + /** @var $imap \Oro\Bundle\ImapBundle\Connector\ImapConnector */ + $imapConnector = $factory->createUserImapConnector($userId); + + // Creating the search query builder + /** @var $queryBuilder \Oro\Bundle\ImapBundle\Connector\Search\SearchQueryBuilder */ + $queryBuilder = $imapConnector->getSearchQueryBuilder(); + + // Building a search query + $query = $queryBuilder + ->from('test@test.com') + ->subject('notification') + ->get(); + + // Request an IMAP server for find emails + $emails = $ewsConnector->findItems('INBOX', $query); +``` diff --git a/src/Oro/Bundle/ImapBundle/Resources/config/services.yml b/src/Oro/Bundle/ImapBundle/Resources/config/services.yml new file mode 100644 index 00000000000..b4482178e56 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Resources/config/services.yml @@ -0,0 +1,22 @@ +parameters: + oro_imap.connector.factory.class: Oro\Bundle\ImapBundle\Connector\ImapConnectorFactory + oro_imap.connector.class: Oro\Bundle\ImapBundle\Connector\ImapConnector + oro_imap.services.factory.class: Oro\Bundle\ImapBundle\Connector\ImapServicesFactory + oro_imap.search_string_manager.gmail.class: Oro\Bundle\ImapBundle\Connector\Search\GmailSearchStringManager + oro_imap.search_string_manager.other.class: Oro\Bundle\ImapBundle\Connector\Search\SearchStringManager + oro_imap.storage.gmail.class: Oro\Bundle\ImapBundle\Mail\Storage\GmailImap + oro_imap.storage.other.class: Oro\Bundle\ImapBundle\Mail\Storage\Imap + +services: + oro_imap.connector.factory: + class: %oro_imap.connector.factory.class% + arguments: + - @oro_imap.services.factory + - %oro_imap.connector.class% + oro_imap.services.factory: + public: false + class: %oro_imap.services.factory.class% + arguments: + - # The configuration of IMAP services. The empty key is used to configure IMAP servers which have not any special preferences + "": [%oro_imap.storage.other.class%, %oro_imap.search_string_manager.other.class%] + X-GM-EXT-1: [%oro_imap.storage.gmail.class%, %oro_imap.search_string_manager.gmail.class%] diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConfigTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConfigTest.php new file mode 100644 index 00000000000..9fbf169a8f5 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConfigTest.php @@ -0,0 +1,47 @@ +assertEquals($host, $obj->getHost()); + $this->assertEquals($port, $obj->getPort()); + $this->assertEquals($ssl, $obj->getSsl()); + $this->assertEquals($user, $obj->getUser()); + $this->assertEquals($password, $obj->getPassword()); + } + + public function testSettersAndGetters() + { + $obj = new ImapConfig(); + + $host = 'testHost'; + $port = 'testPort'; + $ssl = 'testSsl'; + $user = 'testUser'; + $password = 'testPwd'; + + $obj->setHost($host); + $obj->setPort($port); + $obj->setSsl($ssl); + $obj->setUser($user); + $obj->setPassword($password); + + $this->assertEquals($host, $obj->getHost()); + $this->assertEquals($port, $obj->getPort()); + $this->assertEquals($ssl, $obj->getSsl()); + $this->assertEquals($user, $obj->getUser()); + $this->assertEquals($password, $obj->getPassword()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConnectorTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConnectorTest.php new file mode 100644 index 00000000000..2456fcc1602 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapConnectorTest.php @@ -0,0 +1,145 @@ +storage = $this->getMockBuilder('Oro\Bundle\ImapBundle\Mail\Storage\Imap') + ->disableOriginalConstructor() + ->getMock(); + $this->storage->expects($this->any()) + ->method('__destruct'); + + $this->searchStringManager = $this->getMock('Oro\Bundle\ImapBundle\Connector\Search\SearchStringManagerInterface'); + $this->searchStringManager->expects($this->any()) + ->method('isAcceptableItem') + ->will($this->returnValue(true)); + $this->searchStringManager->expects($this->any()) + ->method('buildSearchString') + ->will($this->returnValue('some query')); + + $services = new ImapServices($this->storage, $this->searchStringManager); + + $factory = $this->getMockBuilder('Oro\Bundle\ImapBundle\Connector\ImapServicesFactory') + ->disableOriginalConstructor() + ->setMethods(array('createImapServices')) + ->getMock(); + $factory->expects($this->once()) + ->method('createImapServices') + ->will($this->returnValue($services)); + + $this->connector = new ImapConnector(new ImapConfig(), $factory); + } + + public function testGetSearchQueryBuilder() + { + $builder = $this->connector->getSearchQueryBuilder(); + $this->assertInstanceOf('Oro\Bundle\ImapBundle\Connector\Search\SearchQueryBuilder', $builder); + } + + public function testFindItemsWithNoArguments() + { + $this->storage->expects($this->never()) + ->method('selectFolder'); + $this->storage->expects($this->never()) + ->method('search'); + $this->storage->expects($this->never()) + ->method('getMessage'); + + $result = $this->connector->findItems(); + $this->assertCount(0, $result); + } + + public function testFindItemsWithParentFolderOnly() + { + $this->storage->expects($this->at(0)) + ->method('selectFolder') + ->with($this->equalTo('SomeFolder')); + $this->storage->expects($this->never()) + ->method('search'); + $this->storage->expects($this->never()) + ->method('getMessage'); + + $result = $this->connector->findItems('SomeFolder'); + $this->assertCount(0, $result); + } + + public function testFindItemsWithParentFolderAndSearchQuery() + { + $this->storage->expects($this->at(0)) + ->method('selectFolder') + ->with($this->equalTo('SomeFolder')); + $this->storage->expects($this->at(1)) + ->method('search') + ->with($this->equalTo(array('some query'))) + ->will($this->returnValue(array('1', '2'))); + $this->storage->expects($this->exactly(2)) + ->method('getMessage') + ->will($this->returnValue(new \stdClass())); + + $result = $this->connector->findItems('SomeFolder', $this->connector->getSearchQueryBuilder()->get()); + $this->assertCount(2, $result); + } + + public function testFindFolders() + { + $folder = $this->getMockBuilder('Zend\Mail\Storage\Folder') + ->disableOriginalConstructor() + ->getMock(); + + $this->storage->expects($this->once()) + ->method('getFolders') + ->with($this->equalTo('SomeFolder')) + ->will($this->returnValue($folder)); + + $result = $this->connector->findFolders('SomeFolder'); + $this->assertCount(0, $result); + } + + public function testFindFolder() + { + $folder = $this->getMockBuilder('Zend\Mail\Storage\Folder') + ->disableOriginalConstructor() + ->getMock(); + + $this->storage->expects($this->once()) + ->method('getFolders') + ->with($this->equalTo('SomeFolder')) + ->will($this->returnValue($folder)); + + $result = $this->connector->findFolder('SomeFolder'); + $this->assertTrue($folder === $result); + } + + public function testGetItem() + { + $msg = new \stdClass(); + + $this->storage->expects($this->at(0)) + ->method('getNumberByUniqueId') + ->with($this->equalTo(123)) + ->will($this->returnValue(12345)); + $this->storage->expects($this->at(1)) + ->method('getMessage') + ->with($this->equalTo(12345)) + ->will($this->returnValue($msg)); + + $result = $this->connector->getItem(123); + $this->assertTrue($msg === $result); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapServicesFactoryTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapServicesFactoryTest.php new file mode 100644 index 00000000000..720e97a3a88 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/ImapServicesFactoryTest.php @@ -0,0 +1,62 @@ + array('StorageClass', 'SearchStringManagerClass'))); + } + + public function testCreateImapServicesForDefaultServices() + { + $config = array( + '' => array( + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\Imap1', + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\SearchStringManager1' + ), + 'FEATURE2' => array( + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\Imap2', + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\SearchStringManager2' + ) + ); + + $factory = new ImapServicesFactory($config); + + $services = $factory->createImapServices(new ImapConfig()); + + $expected = new ImapServices(new TestFixtures\Imap1(array()), new TestFixtures\SearchStringManager1()); + + $this->assertEquals($expected, $services); + } + + public function testCreateImapServicesForOtherServices() + { + $config = array( + '' => array( + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\Imap2', + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\SearchStringManager2' + ), + 'FEATURE2' => array( + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\Imap1', + 'Oro\Bundle\ImapBundle\Tests\Unit\Connector\TestFixtures\SearchStringManager1' + ) + ); + + $factory = new ImapServicesFactory($config); + + $services = $factory->createImapServices(new ImapConfig()); + + $expected = new ImapServices(new TestFixtures\Imap1(array()), new TestFixtures\SearchStringManager1()); + + $this->assertEquals($expected, $services); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/GmailSearchStringManagerTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/GmailSearchStringManagerTest.php new file mode 100644 index 00000000000..6cf6486ac99 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/GmailSearchStringManagerTest.php @@ -0,0 +1,167 @@ +searchStringManager = new GmailSearchStringManager(); + $this->query = $this->createSearchQuery(); + } + + /** + * @dataProvider valueProvider + */ + public function testValue($value, $match, $expectedQuery) + { + $this->query->value($value, $match); + $this->assertEquals($expectedQuery, $this->query->convertToSearchString()); + } + + /** + * @dataProvider itemProvider + */ + public function testItem($name, $value, $match, $expectedQuery) + { + $this->query->item($name, $value, $match); + $this->assertEquals($expectedQuery, $this->query->convertToSearchString()); + } + + public function testAndOperator() + { + $this->query->andOperator(); + $this->assertEquals('""', $this->query->convertToSearchString()); + } + + public function testOrOperator() + { + $this->query->orOperator(); + $this->assertEquals('"OR"', $this->query->convertToSearchString()); + } + + public function testNotOperator() + { + $this->query->notOperator(); + $this->assertEquals('"-"', $this->query->convertToSearchString()); + } + + public function testOpenParenthesis() + { + $this->query->openParenthesis(); + $this->assertEquals('"("', $this->query->convertToSearchString()); + } + + public function testCloseParenthesis() + { + $this->query->closeParenthesis(); + $this->assertEquals('")"', $this->query->convertToSearchString()); + } + + public function testComplexQuery() + { + $simpleSubQuery = $this->createSearchQuery(); + $simpleSubQuery->value('val1'); + + $complexSubQuery = $this->createSearchQuery(); + $complexSubQuery->value('val2'); + $complexSubQuery->orOperator(); + $complexSubQuery->value('val3'); + + $this->query->item('subject', $simpleSubQuery); + $this->query->item('subject', $complexSubQuery); + $this->query->orOperator(); + $this->query->openParenthesis(); + $this->query->item('subject', 'product3'); + $this->query->notOperator(); + $this->query->item('subject', 'product4'); + $this->query->closeParenthesis(); + $this->assertEquals( + '"subject:val1 subject:(val2 OR val3) OR (subject:product3 - subject:product4)"', + $this->query->convertToSearchString() + ); + } + + public function valueProvider() + { + $sampleQuery = $this->createSearchQuery(); + $sampleQuery->value('product'); + + return array( + 'one word + DEFAULT_MATCH' => array('product', SearchQueryMatch::DEFAULT_MATCH, '"product"'), + 'one word + SUBSTRING_MATCH' => array('product', SearchQueryMatch::SUBSTRING_MATCH, '"product"'), + 'one word + EXACT_MATCH' => array('product', SearchQueryMatch::EXACT_MATCH, '"+product"'), + 'two words + DEFAULT_MATCH' => array('my product', SearchQueryMatch::DEFAULT_MATCH, '"\\"my product\\""'), + 'two words + SUBSTRING_MATCH' => array('my product', SearchQueryMatch::SUBSTRING_MATCH, '"\\"my product\\""'), + 'two words + EXACT_MATCH' => array('my product', SearchQueryMatch::EXACT_MATCH, '"+\\"my product\\""'), + 'SearchQuery as value + DEFAULT_MATCH' => array($sampleQuery, SearchQueryMatch::DEFAULT_MATCH, '"product"'), + ); + } + + public function itemProvider() + { + $sampleQuery = $this->createSearchQuery(); + $sampleQuery->value('product'); + + return array( + 'one word + DEFAULT_MATCH' => array( + 'subject', + 'product', + SearchQueryMatch::DEFAULT_MATCH, + '"subject:product"' + ), + 'one word + SUBSTRING_MATCH' => array( + 'subject', + 'product', + SearchQueryMatch::SUBSTRING_MATCH, + '"subject:product"' + ), + 'one word + EXACT_MATCH' => array( + 'subject', + 'product', + SearchQueryMatch::EXACT_MATCH, + '"subject:+product"' + ), + 'two words + DEFAULT_MATCH' => array( + 'subject', + 'my product', + SearchQueryMatch::DEFAULT_MATCH, + '"subject:\\"my product\\""' + ), + 'two words + SUBSTRING_MATCH' => array( + 'subject', + 'my product', + SearchQueryMatch::SUBSTRING_MATCH, + '"subject:\\"my product\\""' + ), + 'two words + EXACT_MATCH' => array( + 'subject', + 'my product', + SearchQueryMatch::EXACT_MATCH, + '"subject:+\\"my product\\""' + ), + 'SearchQuery as value + DEFAULT_MATCH' => array( + 'subject', + $sampleQuery, + SearchQueryMatch::DEFAULT_MATCH, + '"subject:product"' + ), + ); + } + + private function createSearchQuery() + { + return new SearchQuery(new GmailSearchStringManager()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryBuilderTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryBuilderTest.php new file mode 100644 index 00000000000..846d14a9fd4 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryBuilderTest.php @@ -0,0 +1,229 @@ +simpleFieldTesting('from', $value, $match); + } + + public function testFromWithClosure() + { + $this->simpleFieldTestingWithClosure('from'); + } + + /** + * @dataProvider simpleProvider + */ + public function testTo($value, $match) + { + $this->simpleFieldTesting('to', $value, $match); + } + + public function testToWithClosure() + { + $this->simpleFieldTestingWithClosure('to'); + } + + /** + * @dataProvider simpleProvider + */ + public function testCc($value, $match) + { + $this->simpleFieldTesting('cc', $value, $match); + } + + public function testCcWithClosure() + { + $this->simpleFieldTestingWithClosure('cc'); + } + + /** + * @dataProvider simpleProvider + */ + public function testBcc($value, $match) + { + $this->simpleFieldTesting('bcc', $value, $match); + } + + public function testBccWithClosure() + { + $this->simpleFieldTestingWithClosure('bcc'); + } + + /** + * @dataProvider simpleProvider + */ + public function testParticipants($value, $match) + { + $this->simpleFieldTesting('participants', $value, $match); + } + + public function testParticipantsWithClosure() + { + $this->simpleFieldTestingWithClosure('participants'); + } + + /** + * @dataProvider simpleProvider + */ + public function testSubject($value, $match) + { + $this->simpleFieldTesting('subject', $value, $match); + } + + public function testSubjectWithClosure() + { + $this->simpleFieldTestingWithClosure('subject'); + } + + /** + * @dataProvider simpleProvider + */ + public function testBody($value, $match) + { + $this->simpleFieldTesting('body', $value, $match); + } + + public function testBodyWithClosure() + { + $this->simpleFieldTestingWithClosure('body'); + } + + /** + * @dataProvider simpleProvider + */ + public function testAttachment($value, $match) + { + $this->simpleFieldTesting('attachment', $value, $match); + } + + public function testAttachmentWithClosure() + { + $this->simpleFieldTestingWithClosure('attachment'); + } + + public function testSent() + { + $this->rangeFieldTesting('sent'); + } + + public function testReceived() + { + $this->rangeFieldTesting('received'); + } + + public static function simpleProvider() + { + return array( + 'default match' => array('product', SearchQueryMatch::DEFAULT_MATCH), + 'substring match' => array('product', SearchQueryMatch::SUBSTRING_MATCH), + 'exact match' => array('product', SearchQueryMatch::EXACT_MATCH), + ); + } + + private function simpleFieldTesting($name, $value, $match) + { + $expr = $this->createSearchQueryBuilder()->$name($value, $match)->get()->getExpression(); + + $expected = array(new SearchQueryExprItem($name, $value, $match)); + + $this->assertEquals($expected, $expr->getItems()); + } + + private function simpleFieldTestingWithClosure($name) + { + /** @var SearchQuery $query */ + $query = $this->createSearchQueryBuilder() + ->$name( + function ($builder) { + /** @var SearchQueryValueBuilder $builder */ + $builder + ->value('val1') + ->value('val2'); + } + ) + ->get(); + $expr = $query->getExpression(); + + $subQuery = $query->newInstance(); + $subQuery->value('val1'); + $subQuery->andOperator(); + $subQuery->value('val2'); + + $expected = array( + new SearchQueryExprItem( + $name, + $subQuery->getExpression(), + SearchQueryMatch::DEFAULT_MATCH + ) + ); + + $this->assertEquals($expected, $expr->getItems()); + } + + private function rangeFieldTesting($name) + { + $expr = $this->createSearchQueryBuilder() + ->$name('val') + ->get() + ->getExpression(); + $expected = array(new SearchQueryExprItem($name . ':after', 'val', SearchQueryMatch::DEFAULT_MATCH)); + $this->assertEquals($expected, $expr->getItems()); + + $expr = $this->createSearchQueryBuilder() + ->$name('val', null) + ->get() + ->getExpression(); + $expected = array(new SearchQueryExprItem($name . ':after', 'val', SearchQueryMatch::DEFAULT_MATCH)); + $this->assertEquals($expected, $expr->getItems()); + + $expr = $this->createSearchQueryBuilder() + ->$name(null, 'val') + ->get() + ->getExpression(); + $expected = array(new SearchQueryExprItem($name . ':before', 'val', SearchQueryMatch::DEFAULT_MATCH)); + $this->assertEquals($expected, $expr->getItems()); + + $expr = $this->createSearchQueryBuilder() + ->$name('val1', 'val2') + ->get() + ->getExpression(); + $expected = array( + new SearchQueryExprItem($name . ':after', 'val1', SearchQueryMatch::DEFAULT_MATCH), + new SearchQueryExprOperator('AND'), + new SearchQueryExprItem($name . ':before', 'val2', SearchQueryMatch::DEFAULT_MATCH) + ); + $this->assertEquals($expected, $expr->getItems()); + } + + /** + * @return SearchQueryBuilder + */ + private function createSearchQueryBuilder() + { + $searchStringManager = $this->getMock('Oro\Bundle\ImapBundle\Connector\Search\SearchStringManagerInterface'); + $searchStringManager + ->expects($this->any()) + ->method('isAcceptableItem') + ->will($this->returnValue(true)); + $searchStringManager + ->expects($this->never()) + ->method('buildSearchString'); + + return new SearchQueryBuilder(new SearchQuery($searchStringManager)); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprItemTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprItemTest.php new file mode 100644 index 00000000000..578ae818616 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprItemTest.php @@ -0,0 +1,36 @@ +assertEquals($name, $obj->getName()); + $this->assertEquals($value, $obj->getValue()); + $this->assertEquals($match, $obj->getMatch()); + } + + public function testSettersAndGetters() + { + $obj = new SearchQueryExprItem('1', '1', '=', 0, false); + + $name = 'testName'; + $value = 'testValue'; + $match = 1; + + $obj->setName($name); + $obj->setValue($value); + $obj->setMatch($match); + + $this->assertEquals($name, $obj->getName()); + $this->assertEquals($value, $obj->getValue()); + $this->assertEquals($match, $obj->getMatch()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprOperatorTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprOperatorTest.php new file mode 100644 index 00000000000..fd6f754699b --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprOperatorTest.php @@ -0,0 +1,27 @@ +assertEquals($name, $obj->getName()); + } + + public function testSettersAndGetters() + { + $obj = new SearchQueryExprOperator('1'); + + $name = 'testName'; + + $obj->setName($name); + + $this->assertEquals($name, $obj->getName()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprValueTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprValueTest.php new file mode 100644 index 00000000000..b75b2d1fd49 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryExprValueTest.php @@ -0,0 +1,31 @@ +assertEquals($value, $obj->getValue()); + $this->assertEquals($match, $obj->getMatch()); + } + + public function testSettersAndGetters() + { + $obj = new SearchQueryExprValue('1', 0); + + $value = 'testValue'; + $match = 1; + + $obj->setValue($value); + $obj->setMatch($match); + + $this->assertEquals($value, $obj->getValue()); + $this->assertEquals($match, $obj->getMatch()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryTest.php new file mode 100644 index 00000000000..75de0a728ae --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchQueryTest.php @@ -0,0 +1,139 @@ +createSearchQuery(); + $query->value($value, $match); + } + + /** + * @dataProvider itemProviderForInvalidArguments + * @expectedException \InvalidArgumentException + */ + public function testItemInvalidArguments($name, $value, $match) + { + $query = $this->createSearchQuery(); + $query->item($name, $value, $match); + } + + /** + * @param SearchQuery $query + * @param $expectedResult + * + * @dataProvider isEmptyProvider + */ + public function testIsEmpty($query, $expectedResult) + { + $this->assertEquals($expectedResult, $query->isEmpty()); + } + + /** + * @param SearchQuery $query + * @param $expectedResult + * + * @dataProvider isComplexProvider + */ + public function testIsComplex($query, $expectedResult) + { + $this->assertEquals($expectedResult, $query->isComplex()); + } + + public function valueProviderForInvalidArguments() + { + $complexQuery = $this->createSearchQuery(); + $complexQuery->value('product1'); + $complexQuery->value('product2'); + + return array( + 'SearchQuery as value + SUBSTRING_MATCH' => array($complexQuery, SearchQueryMatch::SUBSTRING_MATCH), + 'SearchQuery as value + EXACT_MATCH' => array($complexQuery, SearchQueryMatch::EXACT_MATCH), + ); + } + + public function itemProviderForInvalidArguments() + { + $sampleQuery = $this->createSearchQuery(); + $sampleQuery->value('product1'); + $sampleQuery->value('product2'); + + return array( + 'SearchQuery as value + SUBSTRING_MATCH' => array( + 'subject', + $sampleQuery, + SearchQueryMatch::SUBSTRING_MATCH + ), + 'SearchQuery as value + EXACT_MATCH' => array( + 'subject', + $sampleQuery, + SearchQueryMatch::EXACT_MATCH + ), + ); + } + + public function isEmptyProvider() + { + $empty = $this->createSearchQuery(); + $emptyWithEmptySubQuery = $this->createSearchQuery(); + $emptyWithEmptySubQuery->value($this->createSearchQuery()); + $nonEmpty = $this->createSearchQuery(); + $nonEmpty->value('val'); + $nonEmptyWithNonEmptySubQuery = $this->createSearchQuery(); + $nonEmptySubQuery = $this->createSearchQuery(); + $nonEmptySubQuery->value('val'); + $nonEmptyWithNonEmptySubQuery->value($nonEmptySubQuery); + + return array( + "empty" => array($empty, true), + "emptyWithEmptySubQuery" => array($emptyWithEmptySubQuery, true), + "nonEmpty" => array($nonEmpty, false), + "nonEmptyWithNonEmptySubQuery" => array($nonEmptyWithNonEmptySubQuery, false), + ); + } + + public function isComplexProvider() + { + $empty = $this->createSearchQuery(); + $emptyWithEmptySubQuery = $this->createSearchQuery(); + $emptyWithEmptySubQuery->value($this->createSearchQuery()); + + $simple = $this->createSearchQuery(); + $simple->value('val'); + + $complex = $this->createSearchQuery(); + $complex->value('val1'); + $complex->value('val2'); + + return array( + "empty" => array($empty, false), + "emptyWithEmptySubQuery" => array($emptyWithEmptySubQuery, false), + "simple" => array($simple, false), + "complex" => array($complex, true), + ); + } + + private function createSearchQuery() + { + $searchStringManager = $this->getMock('Oro\Bundle\ImapBundle\Connector\Search\SearchStringManagerInterface'); + $searchStringManager + ->expects($this->any()) + ->method('isAcceptableItem') + ->will($this->returnValue(true)); + $searchStringManager + ->expects($this->never()) + ->method('buildSearchString'); + + return new SearchQuery($searchStringManager); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchStringManagerTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchStringManagerTest.php new file mode 100644 index 00000000000..c5a8f2c759d --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/Search/SearchStringManagerTest.php @@ -0,0 +1,229 @@ +searchStringManager = new SearchStringManager(); + $this->query = $this->createSearchQuery(); + } + + /** + * @dataProvider valueProvider + */ + public function testValue($value, $match, $expectedQuery) + { + $this->query->value($value, $match); + $this->assertEquals($expectedQuery, $this->query->convertToSearchString()); + } + + /** + * @dataProvider itemProvider + */ + public function testItem($name, $value, $match, $expectedQuery) + { + $this->query->item($name, $value, $match); + $this->assertEquals($expectedQuery, $this->query->convertToSearchString()); + } + + public function testOrOperatorWithTwoOperands() + { + // val1 OR val2 + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->item('subject', 'val2'); + $this->assertEquals( + 'OR SUBJECT val1 SUBJECT val2', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithNotBeforeFirstOperand() + { + // NOT val1 OR val2 + $this->query->notOperator(); + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->item('subject', 'val2'); + $this->assertEquals( + 'OR NOT SUBJECT val1 SUBJECT val2', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithDoubleNotBeforeFirstOperand() + { + // NOT NOT val1 OR val2 + $this->query->notOperator(); + $this->query->notOperator(); + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->item('subject', 'val2'); + $this->assertEquals( + 'OR NOT NOT SUBJECT val1 SUBJECT val2', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithThreeOperands() + { + // val1 OR val2 OR val3 + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->item('subject', 'val2'); + $this->query->orOperator(); + $this->query->item('subject', 'val3'); + $this->assertEquals( + 'OR SUBJECT val1 OR SUBJECT val2 SUBJECT val3', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithSubQuery() + { + // (val1 OR val2 OR val3) + $subQuery = $this->createSearchQuery(); + $subQuery->value('val1'); + $subQuery->orOperator(); + $subQuery->value('val2'); + $subQuery->orOperator(); + $subQuery->value('val3'); + + $this->query->item('subject', $subQuery); + $this->assertEquals( + '(OR SUBJECT val1 OR SUBJECT val2 SUBJECT val3)', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithParenthesis() + { + // (val1 OR val2) OR val3 + $this->query->openParenthesis(); + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->item('subject', 'val2'); + $this->query->closeParenthesis(); + $this->query->orOperator(); + $this->query->item('subject', 'val3'); + $this->assertEquals( + 'OR (OR SUBJECT val1 SUBJECT val2) SUBJECT val3', + $this->query->convertToSearchString() + ); + } + + public function testOrOperatorWithNestedParenthesis() + { + // (val1 OR (val2 OR val3)) OR val4 + $this->query->openParenthesis(); + $this->query->item('subject', 'val1'); + $this->query->orOperator(); + $this->query->openParenthesis(); + $this->query->item('subject', 'val2'); + $this->query->orOperator(); + $this->query->item('subject', 'val3'); + $this->query->closeParenthesis(); + $this->query->closeParenthesis(); + $this->query->orOperator(); + $this->query->item('subject', 'val4'); + $this->assertEquals( + 'OR (OR SUBJECT val1 (OR SUBJECT val2 SUBJECT val3)) SUBJECT val4', + $this->query->convertToSearchString() + ); + } + + public function testComplexQuery() + { + $simpleSubQuery = $this->createSearchQuery(); + $simpleSubQuery->value('val1'); + + $complexSubQuery = $this->createSearchQuery(); + $complexSubQuery->value('val2'); + $complexSubQuery->orOperator(); + $complexSubQuery->value('val3'); + + $this->query->item('subject', $simpleSubQuery); + $this->query->andOperator(); + $this->query->item('subject', $complexSubQuery); + $this->query->orOperator(); + $this->query->openParenthesis(); + $this->query->item('subject', 'product3'); + $this->query->notOperator(); + $this->query->item('subject', 'product4'); + $this->query->closeParenthesis(); + $this->assertEquals( + 'SUBJECT val1 OR (OR SUBJECT val2 SUBJECT val3) (SUBJECT product3 NOT SUBJECT product4)', + $this->query->convertToSearchString() + ); + } + + public function valueProvider() + { + $sampleQuery = $this->createSearchQuery(); + $sampleQuery->value('product'); + + return array( + 'one word + DEFAULT_MATCH' => array('product', SearchQueryMatch::DEFAULT_MATCH, 'product'), + 'one word + SUBSTRING_MATCH' => array('product', SearchQueryMatch::SUBSTRING_MATCH, 'product'), + 'two words + DEFAULT_MATCH' => array('my product', SearchQueryMatch::DEFAULT_MATCH, '"my product"'), + 'two words + SUBSTRING_MATCH' => array('my product', SearchQueryMatch::SUBSTRING_MATCH, '"my product"'), + 'SearchQuery as value + DEFAULT_MATCH' => array($sampleQuery, SearchQueryMatch::DEFAULT_MATCH, 'product'), + ); + } + + public function itemProvider() + { + $sampleQuery = $this->createSearchQuery(); + $sampleQuery->value('product'); + + return array( + 'one word + DEFAULT_MATCH' => array( + 'subject', + 'product', + SearchQueryMatch::DEFAULT_MATCH, + 'SUBJECT product' + ), + 'one word + SUBSTRING_MATCH' => array( + 'subject', + 'product', + SearchQueryMatch::SUBSTRING_MATCH, + 'SUBJECT product' + ), + 'two words + DEFAULT_MATCH' => array( + 'subject', + 'my product', + SearchQueryMatch::DEFAULT_MATCH, + 'SUBJECT "my product"' + ), + 'two words + SUBSTRING_MATCH' => array( + 'subject', + 'my product', + SearchQueryMatch::SUBSTRING_MATCH, + 'SUBJECT "my product"' + ), + 'SearchQuery as value + DEFAULT_MATCH' => array( + 'subject', + $sampleQuery, + SearchQueryMatch::DEFAULT_MATCH, + 'SUBJECT product' + ), + ); + } + + private function createSearchQuery() + { + return new SearchQuery(new SearchStringManager()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/Imap1.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/Imap1.php new file mode 100644 index 00000000000..629b48dce3d --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Connector/TestFixtures/Imap1.php @@ -0,0 +1,19 @@ +part = $this->getMockBuilder('Zend\Mail\Storage\Part') + ->disableOriginalConstructor() + ->getMock(); + + $this->attachment = new Attachment($this->part); + } + + public function testGetHeaders() + { + $headers = new \stdClass(); + + $this->part + ->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + + $result = $this->attachment->getHeaders(); + + $this->assertTrue($headers === $result); + } + + public function testGetHeader() + { + $header = new \stdClass(); + + $this->part + ->expects($this->once()) + ->method('getHeader') + ->with($this->equalTo('SomeHeader'), $this->equalTo('string')) + ->will($this->returnValue($header)); + + $result = $this->attachment->getHeader('SomeHeader', 'string'); + + $this->assertTrue($header === $result); + } + + public function testGetFileNameWithContentDispositionExists() + { + $testFileName = 'SomeFile'; + $testEncoding = 'SomeEncoding'; + + // Content-Disposition header + $contentDispositionHeader = $this->getMockBuilder('Zend\Mail\Header\GenericHeader') + ->disableOriginalConstructor() + ->getMock(); + $contentDispositionHeader->expects($this->once()) + ->method('getFieldValue') + ->will($this->returnValue('attachment; filename=' . $testFileName)); + $contentDispositionHeader->expects($this->once()) + ->method('getEncoding') + ->will($this->returnValue($testEncoding)); + + // Headers object + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + $headers->expects($this->once()) + ->method('has') + ->with($this->equalTo('Content-Disposition')) + ->will($this->returnValue(true)); + + // Part object + $this->part->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + $this->part + ->expects($this->once()) + ->method('getHeader') + ->with($this->equalTo('Content-Disposition')) + ->will($this->returnValue($contentDispositionHeader)); + + $result = $this->attachment->getFileName(); + + $expected = new Value($testFileName, $testEncoding); + $this->assertEquals($expected, $result); + } + + public function testGetFileNameWithContentDispositionDoesNotExist() + { + $testFileName = 'SomeFile'; + $testEncoding = 'SomeEncoding'; + + // Content-Disposition header + $contentTypeHeader = $this->getMockBuilder('Zend\Mail\Header\ContentType') + ->disableOriginalConstructor() + ->getMock(); + $contentTypeHeader->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('name')) + ->will($this->returnValue($testFileName)); + $contentTypeHeader->expects($this->once()) + ->method('getEncoding') + ->will($this->returnValue($testEncoding)); + + // Headers object + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + $headers->expects($this->any()) + ->method('has') + ->will( + $this->returnValueMap( + array( + array('Content-Disposition', false), + array('Content-Type', false) + ) + ) + ); + + // Part object + $this->part->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + $this->part + ->expects($this->once()) + ->method('getHeader') + ->with($this->equalTo('Content-Type')) + ->will($this->returnValue($contentTypeHeader)); + + $result = $this->attachment->getFileName(); + + $expected = new Value($testFileName, $testEncoding); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider getContentProvider + */ + public function testGetContent($contentTransferEncoding, $contentType, $contentCharset, $contentValue, $expected) + { + // Content-Type header + $contentTypeHeader = $this->getMockBuilder('Zend\Mail\Header\ContentType') + ->disableOriginalConstructor() + ->getMock(); + if ($contentType !== null) { + $contentTypeHeader->expects($this->once()) + ->method('getType') + ->will($this->returnValue($contentType)); + $contentTypeHeader->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('charset')) + ->will($this->returnValue($contentCharset)); + } + + // Content-Transfer-Encoding header + $contentTransferEncodingHeader = $this->getMockBuilder('Zend\Mail\Header\GenericHeader') + ->disableOriginalConstructor() + ->getMock(); + if ($contentTransferEncoding !== null) { + $contentTransferEncodingHeader->expects($this->once()) + ->method('getFieldValue') + ->will($this->returnValue($contentTransferEncoding)); + } + + // Headers object + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + $headers->expects($this->any()) + ->method('has') + ->will( + $this->returnValueMap( + array( + array('Content-Type', $contentType !== null), + array('Content-Transfer-Encoding', $contentTransferEncoding !== null), + ) + ) + ); + + // Part object + $this->part->expects($this->any()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + $this->part + ->expects($this->any()) + ->method('getHeader') + ->will( + $this->returnValueMap( + array( + array('Content-Type', null, $contentTypeHeader), + array('Content-Transfer-Encoding', null, $contentTransferEncodingHeader), + ) + ) + ); + $this->part->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($contentValue)); + + $result = $this->attachment->getContent(); + + $this->assertEquals($expected, $result); + } + + public static function getContentProvider() + { + return array( + '7bit' => array('7Bit', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + '8bit' => array('8Bit', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'binary' => array('Binary', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'base64' => array('Base64', 'SomeContentType', 'SomeCharset', base64_encode('A value'), new Content('A value', 'SomeContentType', 'SomeCharset')), + 'quoted-printable' => array( + 'Quoted-Printable', + 'SomeContentType', + 'SomeCharset', + quoted_printable_encode('A value='), // = symbol is added to test the 'quoted printable' decoding + new Content('A value=', 'SomeContentType', 'SomeCharset') + ), + 'Unknown' => array('Unknown', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'no charset' => array('8Bit', 'SomeContentType', null, 'A value', new Content('A value', 'SomeContentType', 'ASCII')), + 'no Content-Type' => array('8Bit', null, null, 'A value', new Content('A value', 'text/plain', 'ASCII')), + 'no Content-Transfer-Encoding' => array( + null, + 'SomeContentType', + 'SomeCharset', + 'A value', + new Content('A value', 'SomeContentType', 'SomeCharset') + ), + ); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/BodyTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/BodyTest.php new file mode 100644 index 00000000000..b1859121cde --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/BodyTest.php @@ -0,0 +1,346 @@ +part = $this->getMockBuilder('Zend\Mail\Storage\Part') + ->disableOriginalConstructor() + ->getMock(); + + $this->body = new Body($this->part); + } + + public function testGetHeaders() + { + $headers = new \stdClass(); + + $this->part + ->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + + $result = $this->body->getHeaders(); + + $this->assertTrue($headers === $result); + } + + public function testGetHeader() + { + $header = new \stdClass(); + + $this->part + ->expects($this->once()) + ->method('getHeader') + ->with($this->equalTo('SomeHeader'), $this->equalTo('string')) + ->will($this->returnValue($header)); + + $result = $this->body->getHeader('SomeHeader', 'string'); + + $this->assertTrue($header === $result); + } + + public function testGetPartContentType() + { + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + + $headers->expects($this->once()) + ->method('has') + ->with($this->equalTo('Content-Type')) + ->will($this->returnValue(true)); + + $this->part->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + + $header = new \stdClass(); + + $this->part + ->expects($this->once()) + ->method('getHeader') + ->with($this->equalTo('Content-Type')) + ->will($this->returnValue($header)); + + $result = $this->callProtectedMethod($this->body, 'getPartContentType', array($this->part)); + + $this->assertTrue($header === $result); + } + + public function testGetPartContentTypeWithNoContentTypeHeader() + { + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + $headers->expects($this->once()) + ->method('has') + ->with($this->equalTo('Content-Type')) + ->will($this->returnValue(false)); + + $this->part->expects($this->once()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + + + $result = $this->callProtectedMethod($this->body, 'getPartContentType', array($this->part)); + + $this->assertNull($result); + } + + /** + * @dataProvider extractContentProvider + */ + public function testExtractContent($contentTransferEncoding, $contentType, $contentCharset, $contentValue, $expected) + { + // Content-Type header + $contentTypeHeader = $this->getMockBuilder('Zend\Mail\Header\ContentType') + ->disableOriginalConstructor() + ->getMock(); + if ($contentType !== null) { + $contentTypeHeader->expects($this->once()) + ->method('getType') + ->will($this->returnValue($contentType)); + $contentTypeHeader->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('charset')) + ->will($this->returnValue($contentCharset)); + } + + // Content-Transfer-Encoding header + $contentTransferEncodingHeader = $this->getMockBuilder('Zend\Mail\Header\GenericHeader') + ->disableOriginalConstructor() + ->getMock(); + if ($contentTransferEncoding !== null) { + $contentTransferEncodingHeader->expects($this->once()) + ->method('getFieldValue') + ->will($this->returnValue($contentTransferEncoding)); + } + + // Headers object + $headers = $this->getMockBuilder('Zend\Mail\Headers') + ->disableOriginalConstructor() + ->getMock(); + $headers->expects($this->any()) + ->method('has') + ->will( + $this->returnValueMap( + array( + array('Content-Type', $contentType !== null), + array('Content-Transfer-Encoding', $contentTransferEncoding !== null), + ) + ) + ); + + // Part object + $this->part->expects($this->any()) + ->method('getHeaders') + ->will($this->returnValue($headers)); + $this->part + ->expects($this->any()) + ->method('getHeader') + ->will( + $this->returnValueMap( + array( + array('Content-Type', null, $contentTypeHeader), + array('Content-Transfer-Encoding', null, $contentTransferEncodingHeader), + ) + ) + ); + $this->part->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($contentValue)); + + $result = $this->callProtectedMethod($this->body, 'extractContent', array($this->part)); + + $this->assertEquals($expected, $result); + } + + public function testGetContentSinglePartText() + { + $contentValue = 'testContent'; + $contentType = 'testContentType'; + $contentEncoding = 'testEncoding'; + + $bodyPartialMock = $this->getMock( + 'Oro\Bundle\ImapBundle\Mail\Storage\Body', + array('extractContent'), + array($this->part) + ); + $bodyPartialMock->expects($this->once()) + ->method('extractContent') + ->will($this->returnValue(new Content($contentValue, $contentType, $contentEncoding))); + + $this->part->expects($this->once()) + ->method('isMultipart') + ->will($this->returnValue(false)); + + + $result = $bodyPartialMock->getContent(Body::FORMAT_TEXT); + + $expected = new Content($contentValue, $contentType, $contentEncoding); + + $this->assertEquals($expected, $result); + } + + /** + * @expectedException Oro\Bundle\ImapBundle\Mail\Storage\Exception\InvalidBodyFormatException + */ + public function testGetContentSinglePartHtml() + { + $this->part->expects($this->once()) + ->method('isMultipart') + ->will($this->returnValue(false)); + + $this->body->getContent(Body::FORMAT_HTML); + } + + public function testGetContentMultipartText() + { + $maxIterationCount = 3; + $iteratorPos = 0; + + $contentTypeHeader = $this->getMockBuilder('Zend\Mail\Header\ContentType') + ->disableOriginalConstructor() + ->getMock(); + $contentTypeHeader->expects($this->any()) + ->method('getType') + ->will( + $this->returnCallback( + function () use (&$iteratorPos) { + switch ($iteratorPos) { + case 1: + return 'text/plain'; + case 2: + return 'text/html'; + } + return 'other'; + } + ) + ); + + $bodyPartialMock = $this->getMock( + 'Oro\Bundle\ImapBundle\Mail\Storage\Body', + array('extractContent', 'getPartContentType'), + array($this->part) + ); + $bodyPartialMock->expects($this->any()) + ->method('getPartContentType') + ->will($this->returnValue($contentTypeHeader)); + $bodyPartialMock->expects($this->any()) + ->method('extractContent') + ->will( + $this->returnCallback( + function () use (&$iteratorPos) { + return new Content((string)$iteratorPos, 'SomeContentType', 'SomeEncoding'); + } + ) + ); + + $this->part->expects($this->any()) + ->method('isMultipart') + ->will($this->returnValue(true)); + + $this->mockIterator($this->part, $iteratorPos, $maxIterationCount); + + // Test to TEXT body + $result = $bodyPartialMock->getContent(Body::FORMAT_TEXT); + $this->assertEquals(1, $iteratorPos); + $this->assertEquals(new Content('1', 'SomeContentType', 'SomeEncoding'), $result); + + // Test to HTML body + $result = $bodyPartialMock->getContent(Body::FORMAT_HTML); + $this->assertEquals(2, $iteratorPos); + $this->assertEquals(new Content('2', 'SomeContentType', 'SomeEncoding'), $result); + } + + private function mockIterator(\PHPUnit_Framework_MockObject_MockObject $obj, &$iteratorPos, &$maxIterationCount) + { + $obj->expects($this->any()) + ->method('current') + ->will($this->returnValue(null)); + $obj->expects($this->any()) + ->method('next') + ->will( + $this->returnCallback( + function () use (&$iteratorPos) { + $iteratorPos++; + } + ) + ); + $obj->expects($this->any()) + ->method('rewind') + ->will( + $this->returnCallback( + function () use (&$iteratorPos) { + $iteratorPos = 1; + } + ) + ); + $obj->expects($this->any()) + ->method('valid') + ->will( + $this->returnCallback( + function () use (&$iteratorPos, &$maxIterationCount) { + return $iteratorPos < $maxIterationCount; + } + ) + ); + $obj->expects($this->any()) + ->method('key') + ->will( + $this->returnCallback( + function () use (&$iteratorPos) { + return $iteratorPos; + } + ) + ); + } + + private function callProtectedMethod($obj, $methodName, array $args) + { + $class = new \ReflectionClass($obj); + $method = $class->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($obj, $args); + } + + public static function extractContentProvider() + { + return array( + '7bit' => array('7Bit', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + '8bit' => array('8Bit', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'binary' => array('Binary', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'base64' => array('Base64', 'SomeContentType', 'SomeCharset', base64_encode('A value'), new Content('A value', 'SomeContentType', 'SomeCharset')), + 'quoted-printable' => array( + 'Quoted-Printable', + 'SomeContentType', + 'SomeCharset', + quoted_printable_encode('A value='), // = symbol is added to test the 'quoted printable' decoding + new Content('A value=', 'SomeContentType', 'SomeCharset') + ), + 'Unknown' => array('Unknown', 'SomeContentType', 'SomeCharset', 'A value', new Content('A value', 'SomeContentType', 'SomeCharset')), + 'no charset' => array('8Bit', 'SomeContentType', null, 'A value', new Content('A value', 'SomeContentType', 'ASCII')), + 'no Content-Type' => array('8Bit', null, null, 'A value', new Content('A value', 'text/plain', 'ASCII')), + 'no Content-Transfer-Encoding' => array( + null, + 'SomeContentType', + 'SomeCharset', + 'A value', + new Content('A value', 'SomeContentType', 'SomeCharset') + ), + ); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ContentTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ContentTest.php new file mode 100644 index 00000000000..41b146d50c8 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ContentTest.php @@ -0,0 +1,20 @@ +assertEquals($content, $obj->getContent()); + $this->assertEquals($contentType, $obj->getContentType()); + $this->assertEquals($encoding, $obj->getEncoding()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ValueTest.php b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ValueTest.php new file mode 100644 index 00000000000..62e745a9f0e --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/Unit/Mail/Storage/ValueTest.php @@ -0,0 +1,18 @@ +assertEquals($value, $obj->getValue()); + $this->assertEquals($encoding, $obj->getEncoding()); + } +} diff --git a/src/Oro/Bundle/ImapBundle/Tests/bootstrap.php b/src/Oro/Bundle/ImapBundle/Tests/bootstrap.php new file mode 100644 index 00000000000..697a4bfd271 --- /dev/null +++ b/src/Oro/Bundle/ImapBundle/Tests/bootstrap.php @@ -0,0 +1,14 @@ +=5.3.3", + "symfony/symfony": "2.1.*", + "zendframework/zend-mail": "2.1.*" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\GoogleBundle": "" } + }, + "target-dir": "Oro/Bundle/GoogleBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} \ No newline at end of file diff --git a/src/Oro/Bundle/NavigationBundle/Controller/Api/ShortcutsController.php b/src/Oro/Bundle/NavigationBundle/Controller/Api/ShortcutsController.php index 3e59aee7033..c94dd7d80b9 100644 --- a/src/Oro/Bundle/NavigationBundle/Controller/Api/ShortcutsController.php +++ b/src/Oro/Bundle/NavigationBundle/Controller/Api/ShortcutsController.php @@ -20,6 +20,8 @@ */ class ShortcutsController extends FOSRestController { + protected $uris = array(); + /** * REST GET list * @@ -35,24 +37,41 @@ public function getAction($query) { /** @var $provider BuilderChainProvider */ $provider = $this->container->get('oro_menu.builder_chain'); + /** + * merging shortcuts and application menu + */ + $shortcuts = $provider->get('shortcuts'); + $menuItems = $provider->get('application_menu'); + $result = array_merge($this->getResults($shortcuts, $query), $this->getResults($menuItems, $query)); + + return $this->handleView( + $this->view($result, is_array($result) ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND) + ); + } + + /** + * @param ItemInterface $items + * @param $query + * @return array + */ + protected function getResults(ItemInterface $items, $query) + { /** @var $translator TranslatorInterface */ $translator = $this->get('translator'); - $result = array(); - $items = $provider->get('shortcuts'); - /** @var $item ItemInterface */ $itemIterator = new RecursiveItemIterator($items); $iterator = new \RecursiveIteratorIterator($itemIterator, \RecursiveIteratorIterator::SELF_FIRST); + $result = array(); + /** @var $item ItemInterface */ foreach ($iterator as $item) { - if ($item->getExtra('isAllowed')) { + if ($item->getExtra('isAllowed') && !in_array($item->getUri(), $this->uris) && $item->getUri() !== '#') { $key = $translator->trans($item->getLabel()); if (strpos(strtolower($key), strtolower($query)) !== false) { $result[$key] = array('url' => $item->getUri()); + $this->uris[] = $item->getUri(); } } } - return $this->handleView( - $this->view($result, is_array($result) ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND) - ); + return $result; } } diff --git a/src/Oro/Bundle/NavigationBundle/Controller/ShortcutController.php b/src/Oro/Bundle/NavigationBundle/Controller/ShortcutController.php index 75b7f041d16..0ab02de3a8d 100644 --- a/src/Oro/Bundle/NavigationBundle/Controller/ShortcutController.php +++ b/src/Oro/Bundle/NavigationBundle/Controller/ShortcutController.php @@ -18,31 +18,44 @@ */ class ShortcutController extends Controller { + protected $uris = array(); + /** * @Route("actionslist", name="oro_shortcut_actionslist") * @Template */ public function actionslistAction() { - $result = array(); - /** @var $provider BuilderChainProvider */ $provider = $this->container->get('oro_menu.builder_chain'); + /** + * merging shortcuts and application menu + */ + $shortcuts = $provider->get('shortcuts'); + $menuItems = $provider->get('application_menu'); + $result = array_merge($this->getResults($shortcuts), $this->getResults($menuItems)); + ksort($result); + + return array( + 'actionsList' => $result, + ); + } + + protected function getResults(ItemInterface $items) + { /** @var $translator TranslatorInterface */ $translator = $this->get('translator'); - $items = $provider->get('shortcuts'); - /** @var $item ItemInterface */ $itemIterator = new RecursiveItemIterator($items); $iterator = new \RecursiveIteratorIterator($itemIterator, \RecursiveIteratorIterator::SELF_FIRST); + /** @var $item ItemInterface */ foreach ($iterator as $item) { - if ($item->getExtra('isAllowed')) { + if ($item->getExtra('isAllowed') && !in_array($item->getUri(), $this->uris) && $item->getUri() !== '#') { $key = $translator->trans($item->getLabel()); $result[$key] = array('url' => $item->getUri(), 'description' => $item->getExtra('description')); + $this->uris[] = $item->getUri(); } } - return array( - 'actionsList' => $result, - ); + return $result; } } diff --git a/src/Oro/Bundle/NavigationBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/NavigationBundle/DependencyInjection/Configuration.php index ee8983efea5..7583544ee15 100644 --- a/src/Oro/Bundle/NavigationBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/NavigationBundle/DependencyInjection/Configuration.php @@ -30,6 +30,18 @@ public function getConfigTreeBuilder() 'value' => 20, // default value, can be overridden in config.yml 'type' => 'scalar', ), + 'title_suffix' => array( + 'value' => '', // default value, can be overridden in config.yml + 'type' => 'scalar', + ), + 'title_delimiter' => array( + 'value' => '-', // default value, can be overridden in config.yml + 'type' => 'scalar', + ), + 'breadcrumb_menu' => array( + 'value' => 'application_menu', // default value, can be overridden in config.yml + 'type' => 'scalar', + ), ) ); diff --git a/src/Oro/Bundle/NavigationBundle/Event/RequestTitleListener.php b/src/Oro/Bundle/NavigationBundle/Event/RequestTitleListener.php index 79f1db3ee09..3d28797ed5d 100644 --- a/src/Oro/Bundle/NavigationBundle/Event/RequestTitleListener.php +++ b/src/Oro/Bundle/NavigationBundle/Event/RequestTitleListener.php @@ -33,8 +33,10 @@ public function onKernelRequest(GetResponseEvent $event) $request = $event->getRequest(); if (HttpKernel::MASTER_REQUEST != $event->getRequestType() || $request->getRequestFormat() != 'html' - || $request->getMethod() != 'GET' - || ($request->isXmlHttpRequest() && !$request->headers->get('x-oro-hash-navigation')) + || ($request->getMethod() != 'GET' + && !$request->headers->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER)) + || ($request->isXmlHttpRequest() + && !$request->headers->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER)) ) { // don't do anything return; diff --git a/src/Oro/Bundle/NavigationBundle/Event/ResponseHashnavListener.php b/src/Oro/Bundle/NavigationBundle/Event/ResponseHashnavListener.php index 892d06dd1d6..714051454c4 100644 --- a/src/Oro/Bundle/NavigationBundle/Event/ResponseHashnavListener.php +++ b/src/Oro/Bundle/NavigationBundle/Event/ResponseHashnavListener.php @@ -8,6 +8,9 @@ class ResponseHashnavListener { + + const HASH_NAVIGATION_HEADER = 'x-oro-hash-navigation'; + /** * @var \Symfony\Component\Security\Core\SecurityContextInterface */ @@ -33,7 +36,7 @@ public function onResponse(FilterResponseEvent $event) { $request = $event->getRequest(); $response = $event->getResponse(); - if ($request->get('x-oro-hash-navigation') || $request->headers->get('x-oro-hash-navigation')) { + if ($request->get(self::HASH_NAVIGATION_HEADER) || $request->headers->get(self::HASH_NAVIGATION_HEADER)) { $location = ''; $isFullRedirect = false; if ($response->isRedirect()) { diff --git a/src/Oro/Bundle/NavigationBundle/Event/ResponseHistoryListener.php b/src/Oro/Bundle/NavigationBundle/Event/ResponseHistoryListener.php index b9a1e86afef..10c06cbd029 100644 --- a/src/Oro/Bundle/NavigationBundle/Event/ResponseHistoryListener.php +++ b/src/Oro/Bundle/NavigationBundle/Event/ResponseHistoryListener.php @@ -111,7 +111,8 @@ private function matchRequest(Response $response, Request $request) return !($response->getStatusCode() != 200 || $request->getRequestFormat() != 'html' || $request->getMethod() != 'GET' - || ($request->isXmlHttpRequest() && !$request->headers->get('x-oro-hash-navigation')) + || ($request->isXmlHttpRequest() + && !$request->headers->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER)) || $route[0] == '_' || $route == 'oro_default' || is_null($this->user)); diff --git a/src/Oro/Bundle/NavigationBundle/Menu/AclAwareMenuFactoryExtension.php b/src/Oro/Bundle/NavigationBundle/Menu/AclAwareMenuFactoryExtension.php index 70dbb758c68..81b34be08af 100644 --- a/src/Oro/Bundle/NavigationBundle/Menu/AclAwareMenuFactoryExtension.php +++ b/src/Oro/Bundle/NavigationBundle/Menu/AclAwareMenuFactoryExtension.php @@ -18,7 +18,6 @@ class AclAwareMenuFactoryExtension implements Factory\ExtensionInterface const ROUTE_CONTROLLER_KEY = '_controller'; const CONTROLLER_ACTION_DELIMITER = '::'; const DEFAULT_ACL_POLICY = true; - const CACHE_NAMESPACE = 'oro_menu.cache'; /**#@-*/ /** @@ -59,7 +58,6 @@ public function __construct(RouterInterface $router, Manager $aclManager) public function setCache(CacheProvider $cache) { $this->cache = $cache; - $this->cache->setNamespace(self::CACHE_NAMESPACE); } /** diff --git a/src/Oro/Bundle/NavigationBundle/Menu/BreadcrumbManager.php b/src/Oro/Bundle/NavigationBundle/Menu/BreadcrumbManager.php new file mode 100644 index 00000000000..9beea847592 --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Menu/BreadcrumbManager.php @@ -0,0 +1,218 @@ +matcher = $matcher; + $this->provider = $provider; + $this->router = $router; + } + + /** + * Get breadcrumbs for current menu item + * + * @param $menuName + * @param bool $isInverse + * @return array + */ + public function getBreadcrumbs($menuName, $isInverse = true) + { + $menu = $this->getMenu($menuName); + $currentItem = $this->getCurrentMenuItem($menu); + + if ($currentItem) { + + return $this->getBreadcrumbArray($menuName, $currentItem, $isInverse); + } + } + + /** + * Retrieves item in the menu, eventually using the menu provider. + * + * @param ItemInterface|string $menu + * @param array $pathName + * @param array $options + * + * @return ItemInterface + * + * @throws \LogicException + * @throws \InvalidArgumentException when the path is invalid + */ + public function getMenu($menu, array $pathName = array(), array $options = array()) + { + if (!$menu instanceof ItemInterface) { + $menu = $this->provider->get((string) $menu, $options); + } + foreach ($pathName as $child) { + $menu = $menu->getChild($child); + if ($menu === null) { + throw new \InvalidArgumentException(sprintf('The menu has no child named "%s"', $child)); + } + } + + return $menu; + } + + /** + * Find current menu item + * + * @param $menu + * @return null|ItemInterface + */ + public function getCurrentMenuItem($menu) + { + foreach ($menu as $item) { + if ($this->matcher->isCurrent($item)) { + return $item; + } + + if ($item->getChildren() && $currentChild = $this->getCurrentMenuItem($item)) { + return $currentChild; + } + } + + return null; + } + + /** + * Find menu item by route + * + * @param $menu + * @param $route + * @return ItemInterface + */ + public function getMenuItemByRoute($menu, $route) + { + foreach ($menu as $item) { + /** @var $item ItemInterface */ + + $routes = (array)$item->getExtra('routes', array()); + if ($this->match($routes, $route)) { + return $item; + } + + if ($item->getChildren() && $currentChild = $this->getMenuItemByRoute($item, $route)) { + return $currentChild; + } + } + } + + + + /** + * Return breadcrumb array + * + * @param $menuName + * @param $item + * @param bool $isInverse + * @return array + */ + public function getBreadcrumbArray($menuName, $item, $isInverse = true) + { + $manipulator = new MenuManipulator(); + $breadcrumbs = $manipulator->getBreadcrumbsArray($item); + if ($breadcrumbs[0]['label'] == $menuName) { + unset($breadcrumbs[0]); + } + + if (!$isInverse) { + $breadcrumbs = array_reverse($breadcrumbs); + } + + return $breadcrumbs; + } + + /** + * Get menu item breadcrumbs list + * + * @param $menu + * @param $route + * @return array + */ + public function getBreadcrumbLabels($menu, $route) + { + $labels = array(); + $menuItem = $this->getMenuItemByRoute($this->getMenu($menu), $route); + if ($menuItem) { + $breadcrumb = $this->getBreadcrumbArray($menu, $menuItem, false); + foreach ($breadcrumb as $breadcrumbItem) { + $labels[] = $breadcrumbItem['label']; + } + } + + return $labels; + } + + /** + * Match routes + * + * @param array $routes + * @param $route + * @return bool + */ + protected function match(array $routes, $route) + { + foreach ($routes as $testedRoute) { + if (!$this->routeMatch($testedRoute, $route)) { + continue; + } + + return true; + } + + return false; + } + + /** + * Match routes + * + * @param string $pattern + * @param string $route + * @return boolean + */ + protected function routeMatch($pattern, $route) + { + if ($pattern == $route) { + + return true; + } elseif (0 === strpos($pattern, '/') && strlen($pattern) - 1 === strrpos($pattern, '/')) { + + return preg_match($pattern, $route); + } elseif (false !== strpos($pattern, '*')) { + $pattern = sprintf('/^%s$/', str_replace('*', '\w+', $pattern)); + + return preg_match($pattern, $route); + } else { + + return false; + } + } +} diff --git a/src/Oro/Bundle/NavigationBundle/Provider/TitleService.php b/src/Oro/Bundle/NavigationBundle/Provider/TitleService.php index da1b843fd54..b42d1d57b3c 100644 --- a/src/Oro/Bundle/NavigationBundle/Provider/TitleService.php +++ b/src/Oro/Bundle/NavigationBundle/Provider/TitleService.php @@ -12,6 +12,8 @@ use Oro\Bundle\NavigationBundle\Title\TitleReader\ConfigReader; use Oro\Bundle\NavigationBundle\Title\TitleReader\AnnotationsReader; use Oro\Bundle\NavigationBundle\Title\StoredTitle; +use Oro\Bundle\NavigationBundle\Menu\BreadcrumbManager; +use Oro\Bundle\ConfigBundle\Config\UserConfigManager; use Doctrine\ORM\EntityRepository; @@ -70,18 +72,31 @@ class TitleService implements TitleServiceInterface /** @var array */ protected $titles = null; + /** + * @var BreadcrumbManager + */ + protected $breadcrumbManager; + + /** + * @var UserConfigManager + */ + protected $userConfigManager; + public function __construct( AnnotationsReader $reader, ConfigReader $configReader, Translator $translator, ObjectManager $em, - Serializer $serializer + Serializer $serializer, + UserConfigManager $userConfigManager, + BreadcrumbManager $breadcrumbManager ) { $this->readers = array($reader, $configReader); - $this->translator = $translator; $this->em = $em; $this->serializer = $serializer; + $this->userConfigManager = $userConfigManager; + $this->breadcrumbManager = $breadcrumbManager; } /** @@ -299,6 +314,10 @@ public function update($routes) // update existing system titles if ($entity->getIsSystem()) { + $title = $this->createTile($route, $title); + if (!$title) { + $title = ''; + } $entity->setTitle($title); $this->em->persist($entity); } @@ -308,17 +327,46 @@ public function update($routes) // create title items for new routes foreach ($data as $route => $title) { - $entity = new Title(); - $entity->setTitle($title instanceof Route ? '' : $title); - $entity->setRoute($route); - $entity->setIsSystem(true); + if ($title = $this->createTile($route, $title)) { + $entity = new Title(); + $entity->setTitle($title); + $entity->setRoute($route); + $entity->setIsSystem(true); - $this->em->persist($entity); + $this->em->persist($entity); + } } $this->em->flush(); } + protected function createTile($route, $title) + { + if (!($title instanceof Route)) { + $titleData = array(); + + if ($title) { + $titleData[] = $title; + } + + $breadcrumbLabels = $this->breadcrumbManager->getBreadcrumbLabels( + $this->userConfigManager->get('oro_navigation.breadcrumb_menu'), + $route + ); + if (count($breadcrumbLabels)) { + $titleData = array_merge($titleData, $breadcrumbLabels); + } + + if ($globalTitleSuffix = $this->userConfigManager->get('oro_navigation.title_suffix')) { + $titleData[] = $globalTitleSuffix; + } + + return implode(' ' . $this->userConfigManager->get('oro_navigation.title_delimiter') . ' ', $titleData); + } + + return false; + } + /** * Return serialized title data * diff --git a/src/Oro/Bundle/NavigationBundle/Resources/config/navigation.yml b/src/Oro/Bundle/NavigationBundle/Resources/config/navigation.yml index a3d4c8af6e5..715520348ed 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/NavigationBundle/Resources/config/navigation.yml @@ -4,6 +4,7 @@ oro_menu_config: template: 'OroNavigationBundle:Menu:application_menu.html.twig' currentClass: 'active' ancestorClass: 'active' + rootClass: 'nav nav-multilevel' dots_menu: template: 'OroNavigationBundle:Menu:dots_menu.html.twig' shortcuts: @@ -27,13 +28,6 @@ oro_menu_config: currentAsLink: false items: - home: - name: 'Home' - label: '' - extras: - icon: 'icon-home icon-white' - position: 1 - route: 'oro_default' pinbar: label: 'Pinbar' extras: @@ -60,8 +54,6 @@ oro_menu_config: tree: application_menu: type: application_menu - children: - home: ~ dots_menu: type: dots_menu diff --git a/src/Oro/Bundle/NavigationBundle/Resources/config/placeholders.yml b/src/Oro/Bundle/NavigationBundle/Resources/config/placeholders.yml index 77972273e83..ad4966b5336 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/config/placeholders.yml +++ b/src/Oro/Bundle/NavigationBundle/Resources/config/placeholders.yml @@ -30,16 +30,23 @@ placeholders: items: application_menu: order: 100 - pinbar: - label: Pinbar + before_navigation: + label: Top Navigation Before items: dots_menu: order: 100 + after_navigation: + label: Top Navigation After pin_button: label: Pinbar buttons items: pin_button: order: 100 + breadcrumb: + label: Breadcrumbs + items: + breadcrumbs: + order: 100 items: hashNavigation: @@ -58,3 +65,5 @@ items: template: OroNavigationBundle:ApplicationMenu:dotsMenu.html.twig pin_button: template: OroNavigationBundle:ApplicationMenu:pinButton.html.twig + breadcrumbs: + template: OroNavigationBundle:ApplicationMenu:breabcrumbs.html.twig diff --git a/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml b/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml index 2bb259edaa1..51ef3038e9a 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml @@ -34,14 +34,21 @@ parameters: oro_navigation.form.type.pagestate.class: Oro\Bundle\NavigationBundle\Form\Type\PageStateType oro_navigation.form.handler.pagestate.class: Oro\Bundle\NavigationBundle\Form\Handler\PageStateHandler + oro_navigation.breadcrumbs_manager.class: Oro\Bundle\NavigationBundle\Menu\BreadcrumbManager + services: + oro_menu.cache: + parent: oro.cache.abstract + calls: + - [ setNamespace, [ "oro_menu.cache" ] ] + oro_menu_acl_extension: class: %oro_menu.factory.acl_extension.class% arguments: - @router - @oro_user.acl_manager calls: - - [ setCache, [ @cache ] ] + - [ setCache, [ @oro_menu.cache ] ] oro_menu.factory: class: %oro_menu.factory.class% @@ -68,6 +75,7 @@ services: arguments: - @knp_menu.helper - @oro_menu.builder_chain + - @oro_navigation.breadcrumb_manager - @service_container tags: - { name: twig.extension } @@ -178,6 +186,8 @@ services: - @translator.default - @doctrine.orm.entity_manager - @serializer + - @oro_config.user + - @oro_navigation.breadcrumb_manager calls: - [ setTitles, [ %oro_titles% ] ] @@ -211,8 +221,6 @@ services: tags: - { name: translation.extractor, alias: navigation_translation_extractor } - - oro_navigation.form.pagestate: class: Symfony\Component\Form\Form factory_method: createNamed @@ -240,3 +248,10 @@ services: tags: - { name: twig.extension } - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } + + oro_navigation.breadcrumb_manager: + class: %oro_navigation.breadcrumbs_manager.class% + arguments: + - @oro_menu.builder_chain + - @knp_menu.matcher + - @router \ No newline at end of file diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js index ad86eabbb6c..8bdf30970a1 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js @@ -26,12 +26,13 @@ Oro.Navigation = Backbone.Router.extend({ * mostViwedTab - Selector for most viewed 3 dots menu tab * flashMessages - Selector for system messages block * menu - Selector for system main menu + * breadcrumb - Selector for breadcrumb block * pinButton - Selector for pin, close and favorite buttons div * * @property */ selectors: { - links: 'a:not([href^=#],[href^=javascript]),span[data-url]', + links: 'a:not([href^=#],[href^=javascript],[href^=mailto]),span[data-url]', scrollLinks: 'a[href^=#]', forms: 'form', content: '#content', @@ -44,6 +45,7 @@ Oro.Navigation = Backbone.Router.extend({ mostViewedTab: '#mostviewed-content', flashMessages: '#flash-messages', menu: '#main-menu', + breadcrumb: '#breadcrumb', pinButton: '#pin-button-div' }, selectorCached: {}, @@ -54,6 +56,12 @@ Oro.Navigation = Backbone.Router.extend({ /** @property {String} */ baseUrl: '', + /** @property {String} */ + headerId: '', + + /** @property {Object} */ + headerObject: '', + /** * State data for grids * @@ -100,6 +108,8 @@ Oro.Navigation = Backbone.Router.extend({ notificationMessage: null, + outdatedMessage: '', + /** * Routing default action * @@ -159,7 +169,11 @@ Oro.Navigation = Backbone.Router.extend({ throw new TypeError("'baseUrl' is required"); } - this.baseUrl = options.baseUrl; + this.baseUrl = options.baseUrl; + this.headerId = options.headerId; + var header = {}; + header[this.headerId] = true; + this.headerObject = header; if (window.location.hash === '') { //skip ajax page refresh for the current page this.skipAjaxCall = true; @@ -187,7 +201,7 @@ Oro.Navigation = Backbone.Router.extend({ var useCache = this.useCache; $.ajax({ url: pageUrl, - headers: { 'x-oro-hash-navigation': true }, + headers: this.headerObject, beforeSend: function( xhr ) { //remove standard ajax header because we already have a custom header sent xhr.setRequestHeader('X-Requested-With', {toString: function(){ return ''; }}); @@ -197,6 +211,7 @@ Oro.Navigation = Backbone.Router.extend({ this.showError('Error Message: ' + textStatus, 'HTTP Error: ' + errorThrown); this.updateDebugToolbar(jqXHR); this.afterRequest(); + this.loadingMask.hide(); }, this), success: _.bind(function (data, textStatus, jqXHR) { @@ -234,6 +249,7 @@ Oro.Navigation = Backbone.Router.extend({ }, initCacheTimer: function() { + this.clearCacheTimer(); this.cacheTimer = setInterval(_.bind(function() { var cacheData = this.getCachedData(); if (cacheData) { @@ -255,9 +271,12 @@ Oro.Navigation = Backbone.Router.extend({ validateMd5Request: function(cacheData) { var pageUrl = this.baseUrl + this.url; var url = this.url; + var params = {}; + params[this.headerId] = true; + params['hash-navigation-md5'] = true; $.ajax({ url: pageUrl, - data:{'hash-navigation-md5' : true, 'x-oro-hash-navigation' : true}, + data: params, error: _.bind(function (jqXHR, textStatus, errorThrown) { }, this), @@ -315,9 +334,13 @@ Oro.Navigation = Backbone.Router.extend({ showOutdatedMessage: function(url) { this.clearCacheTimer(); if (this.useCache && this.url == url) { - var message = Translator.get("Content of the page is outdated, please %click here% to refresh the page"); - message = message.replace(/%(.*)%/,"$1"); - this.notificationMessage = Oro.NotificationMessage('warning', message); + if (!this.notificationMessage) { + var message = _.__("Content of the page is outdated, please %click here% to refresh the page"); + this.outdatedMessage = message.replace(/%(.*)%/,"$1"); + } else { + this.notificationMessage.close(); + } + this.notificationMessage = Oro.NotificationMessage('warning', this.outdatedMessage); } }, @@ -394,7 +417,6 @@ Oro.Navigation = Backbone.Router.extend({ * * @param objectName * @param state - * @param url */ updateCachedContent: function(objectName, state) { if (this.tempCache.states) { @@ -453,10 +475,6 @@ Oro.Navigation = Backbone.Router.extend({ * Init */ init: function() { - /** - * Processing all links - */ - this.processClicks(this.selectorCached.links); /** * Processing all links in grid after grid load */ @@ -503,7 +521,7 @@ Oro.Navigation = Backbone.Router.extend({ Oro.Events.bind( "datagrid_filters:rendered", function (collection) { - if (this.getCachedData()) { + if (this.getCachedData() && this.encodedStateData) { collection.trigger('updateState', collection); } }, @@ -607,12 +625,12 @@ Oro.Navigation = Backbone.Router.extend({ ); this.confirmModal = new Oro.BootstrapModal({ - title: Translator.get('Refresh Confirmation'), - content: Translator.get('Your local changes will be lost. Are you sure you want to refresh the page?'), - okText: 'Ok, got it.', + title: _.__('Refresh Confirmation'), + content: _.__('Your local changes will be lost. Are you sure you want to refresh the page?'), + okText: _.__('Ok, got it.'), className: 'modal modal-primary', okButtonClass: 'btn-primary btn-large', - cancelText: Translator.get('Cancel') + cancelText: _.__('Cancel') }); this.confirmModal.on('ok', _.bind(function() { this.refreshPage(); @@ -637,6 +655,12 @@ Oro.Navigation = Backbone.Router.extend({ }, this) ); + /** + * Processing all links + */ + this.processClicks(this.selectorCached.links); + this.disableEmptyLinks(this.selectorCached.menu.find(this.selectors.scrollLinks)); + this.processForms(this.selectors.forms); this.processAnchors(this.selectorCached.container.find(this.selectors.scrollLinks)); @@ -666,7 +690,6 @@ Oro.Navigation = Backbone.Router.extend({ * Triggered after hash navigation ajax request */ afterRequest: function() { - this.loadingMask.hide(); this.formState = ''; this.initCacheTimer(); }, @@ -768,6 +791,7 @@ Oro.Navigation = Backbone.Router.extend({ var content = data.content; this.selectorCached.container.html(content); this.selectorCached.menu.html(data.mainMenu); + this.selectorCached.breadcrumb.html(data.breadcrumb); /** * Collecting javascript from head and append them to content */ @@ -787,6 +811,7 @@ Oro.Navigation = Backbone.Router.extend({ $('.top-action-box .btn').filter('.minimize-button, .favorite-button').data('title', titleSerialized); } this.processClicks(this.selectorCached.menu.find(this.selectors.links)); + this.disableEmptyLinks(this.selectorCached.menu.find(this.selectors.scrollLinks)); this.processClicks(this.selectorCached.container.find(this.selectors.links)); this.processAnchors(this.selectorCached.container.find(this.selectors.scrollLinks)); this.processForms(this.selectorCached.container.find(this.selectors.forms)); @@ -795,9 +820,10 @@ Oro.Navigation = Backbone.Router.extend({ if (!options.fromCache) { this.updateMenuTabs(data); this.addMessages(data.flashMessages); - Oro.Events.trigger("hash_navigation_request:refresh", this); } this.hideActiveDropdowns(); + Oro.Events.trigger("hash_navigation_request:refresh", this); + this.loadingMask.hide(); } } } @@ -808,12 +834,24 @@ Oro.Navigation = Backbone.Router.extend({ if (Oro.debug) { document.body.innerHTML = rawData; } else { - this.showError('', Translator.get("Sorry, page was not loaded correctly")); + this.showError('', _.__('Sorry, page was not loaded correctly')); } } this.triggerCompleteEvent(); }, + /** + * Disable # links to prevent hash changing + * + * @param selector + */ + disableEmptyLinks: function(selector) { + $(selector).on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + }) + }, + processRedirect: function (data) { var redirectUrl = data.location; var urlParts = redirectUrl.split('url='); @@ -998,11 +1036,12 @@ Oro.Navigation = Backbone.Router.extend({ } else { this.beforeRequest(); $(target).ajaxSubmit({ - data:{'x-oro-hash-navigation' : true}, - headers: { 'x-oro-hash-navigation': true }, + data: this.headerObject, + headers: this.headerObject, error: _.bind(function (XMLHttpRequest, textStatus, errorThrown) { this.showError('Error Message: ' + textStatus, 'HTTP Error: ' + errorThrown); this.afterRequest(); + this.loadingMask.hide(); }, this), success: _.bind(function (data) { this.handleResponse(data, {'skipCache' : true}); //don't cache form submit response diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/favorites.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/favorites.js index d3e8022ba4b..24921945a27 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/favorites.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/favorites.js @@ -45,9 +45,22 @@ navigation.favorites.MainView = navigation.MainViewAbstract.extend({ }, toggleItem: function(e) { + var self = this; var current = this.getItemForCurrentPage(); if (current.length) { - _.each(current, function(item) {item.destroy({wait: true});}); + _.each(current, function(item) { + item.destroy({ + wait: false, // This option affects correct disabling of favorites icon + error: function(model, xhr, options) { + if (xhr.status == 404 && !Oro.debug) { + // Suppress error if it's 404 response and not debug mode + self.inactivate(); + } else { + Oro.BackboneError.Dispatch(model, xhr, options); + } + } + }); + }); } else { var itemData = this.getNewItemData(Backbone.$(e.currentTarget)); itemData['type'] = 'favorite'; diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pagestate.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pagestate.js index 188d26c21df..20f0b8b8d40 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pagestate.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pagestate.js @@ -23,7 +23,7 @@ Oro.PageState.View = Backbone.View.extend({ * Init page state after hash navigation request is completed */ Oro.Events.bind( - "hash_navigation_request:complete", + "hash_navigation_request:refresh", function() { this.stopCollecting = false; this.init(); @@ -48,8 +48,6 @@ Oro.PageState.View = Backbone.View.extend({ }, init: function() { - var self = this; - this.clearTimer(); if (!this.hasForm()) { return; @@ -57,20 +55,21 @@ Oro.PageState.View = Backbone.View.extend({ Backbone.$.get( Routing.generate('oro_api_get_pagestate_checkid') + '?pageId=' + this.filterUrl(), - function (data) { - self.model.set({ + _.bind(function (data) { + this.clearTimer(); + this.model.set({ id : data.id, pagestate : data.pagestate }); - if (parseInt(data.id) > 0 && self.model.get('restore') && self.needServerRestore) { - self.restore(); + if (parseInt(data.id) > 0 && this.model.get('restore') && this.needServerRestore) { + this.restore(); } - Oro.PageStateTimer = setInterval(function() { - self.collect(); - }, 2000); - } + Oro.PageStateTimer = setInterval(_.bind(function() { + this.collect(); + }, this), 2000); + }, this) ) }, diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.item.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.item.js index 866ec13566a..64deec3c0c3 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.item.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.item.js @@ -44,7 +44,17 @@ navigation.pinbar.ItemView = Backbone.View.extend({ unpin: function() { Oro.Events.trigger("pinbar_item_remove_before", this.model); - this.model.destroy({wait: true}); + this.model.destroy({ + wait: true, + error: _.bind(function(model, xhr, options) { + if (xhr.status == 404 && !Oro.debug) { + // Suppress error if it's 404 response and not debug mode + this.removeItem(); + } else { + Oro.BackboneError.Dispatch(model, xhr, options); + } + }, this) + }); return false; }, diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.js index a3a1683e3c8..dc4f928664a 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/views/pinbar.js @@ -9,8 +9,6 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ el: '.pin-bar', listBar: '.list-bar', minimizeButton: '.top-action-box .minimize-button', - closeButton: '.top-action-box .close-button', - history: [], defaultUrl: '/', tabId: 'pinbar', collection: navigation.pinbar.Items @@ -26,7 +24,7 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ initialize: function() { this.$listBar = this.getBackboneElement(this.options.listBar); this.$minimizeButton = Backbone.$(this.options.minimizeButton); - this.$closeButton = Backbone.$(this.options.closeButton); + this.$icon = this.$minimizeButton.find('i'); this.listenTo(this.options.collection, 'add', function(item) {this.setItemPosition(item)}); this.listenTo(this.options.collection, 'remove', this.onPageClose); @@ -42,33 +40,26 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ */ Oro.Events.bind( "grid_load:complete", - function () { - this.updatePinbarState(); - }, + this.updatePinbarState, + this + ); + + /** + * Change pinbar icon state after hash navigation request is completed + */ + Oro.Events.bind( + "hash_navigation_request:complete", + this.checkPinbarIcon, this ); this.$minimizeButton.click(_.bind(this.minimizePage, this)); - this.$closeButton.click(_.bind(this.closePage, this)); this.registerTab(); this.cleanup(); this.render(); }, - /** - * Get previous maximized URL - * - * @return {*} - */ - getLatestUrl: function() { - if (this.options.history.length) { - return _.last(this.options.history); - } else { - return this.options.defaultUrl; - } - }, - /** * Get backbone DOM element * @@ -87,22 +78,22 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ handleItemStateChange: function(item) { if (!this.massAdd) { var url = null; - var goBack = false; - if (item.get('maximized')) { + var changeLocation = item.get('maximized'); + if (changeLocation) { url = item.get('url'); - this.removeFromHistory(item); - this.options.history.push(this.cleanupUrl(url)); - } else { - goBack = true; } if (this.cleanupUrl(url) != this.cleanupUrl(this.getCurrentPageItemData().url)) { + if (Oro.hashNavigationEnabled() && changeLocation) { + Oro.hashNavigationInstance.setLocation(url, {useCache: true}); + } item.save( null, { wait: true, success: _.bind(function () { - if (!goBack) { - Oro.hashNavigationInstance.setLocation(url, {useCache: true}); + this.checkPinbarIcon(); + if (!Oro.hashNavigationEnabled() && changeLocation) { + window.location.href = url; } }, this) } @@ -111,23 +102,19 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ } }, - /** - * Remove item from history - * - * @param item - */ - removeFromHistory: function(item) { - var currentItemUrl = this.cleanupUrl(item.get('url')) - this.options.history = _.filter(this.options.history, function (url) { - return url != currentItemUrl; - }); + checkPinbarIcon: function() { + if (this.getItemForCurrentPage().length) { + this.activate(); + } else { + this.inactivate(); + } }, /** * Handle page close */ onPageClose: function(item) { - this.removeFromHistory(item); + this.checkPinbarIcon(); this.reorder(); }, @@ -142,7 +129,6 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ var pinnedItem = this.getItemForCurrentPage(true); if (pinnedItem.length) { _.each(pinnedItem, function(item) { - this.removeFromHistory(item); item.set('maximized', false); }, this); } else { @@ -229,6 +215,14 @@ navigation.pinbar.MainView = navigation.MainViewAbstract.extend({ }); }, + activate: function() { + this.$icon.addClass('icon-gold'); + }, + + inactivate: function() { + this.$icon.removeClass('icon-gold'); + }, + /** * Choose container and add item to it. * diff --git a/src/Oro/Bundle/NavigationBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/NavigationBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..95d95b9a11c --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,5 @@ +"Sorry, page was not loaded correctly": "Sorry, page was not loaded correctly" +"Your local changes will be lost. Are you sure you want to refresh the page?": "Your local changes will be lost. Are you sure you want to refresh the page?" +"Cancel": "Cancel" +"Content of the page is outdated, please %click here% to refresh the page": "Content of the page is outdated, please %click here% to refresh the page" +"Ok, got it.": "Ok, got it." \ No newline at end of file diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/breabcrumbs.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/breabcrumbs.html.twig new file mode 100644 index 00000000000..f4215f65a6c --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/breabcrumbs.html.twig @@ -0,0 +1 @@ +{{ oro_breadcrumbs('application_menu') }} \ No newline at end of file diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/pinButton.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/pinButton.html.twig index c6f2d98ef93..ac64abe6eef 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/pinButton.html.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/ApplicationMenu/pinButton.html.twig @@ -1,12 +1,8 @@
    diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/hashNavAjax.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/hashNavAjax.html.twig index 074f37c2cbd..d4c2306ed84 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/hashNavAjax.html.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/hashNavAjax.html.twig @@ -9,6 +9,7 @@ 'titleSerialized': oro_title_render_serialized(), 'scripts': script, 'mainMenu': menu, + 'breadcrumb': breadcrumb, 'flashMessages': app.session.flashbag.all, 'history': oro_menu_render('history'), 'mostviewed': oro_menu_render('mostviewed'), diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/script.js.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/script.js.twig index 548f5c875d9..00d8b3c8a5c 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/script.js.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/HashNav/script.js.twig @@ -1,7 +1,10 @@ diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/application_menu.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/application_menu.html.twig index 9508f608b92..3ac5f2e0cc3 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/application_menu.html.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/application_menu.html.twig @@ -1,22 +1,19 @@ -{% extends 'OroNavigationBundle:Menu:horizontal_tabs.html.twig' %} +{% extends 'OroNavigationBundle:Menu:menu.html.twig' %} -{% block root %} - {% if item.hasChildren and options.depth is not sameas(0) and item.displayChildren %} - -
    - -
    +{% block item %} + {% if item.hasChildren and item.displayChildren %} + {%- set classes = classes|merge(['dropdown']) %} + {%- set childrenClasses = childrenClasses|merge(['dropdown-menu']) %} + {% endif %} + {{ block('item_renderer') }} +{% endblock %} -
    - {{ block('navbar_tabs_content') }} -
    +{% block linkElement %} + {% import 'OroNavigationBundle:Menu:menu.html.twig' as oro_menu %} + {% if item.hasChildren and item.displayChildren and item.level is sameas(0)%} + {% set linkAttributes = linkAttributes|merge( + {'class': oro_menu.add_attribute_values(linkAttributes, 'class', ['dropdown-toggle']), 'data-toggle': 'dropdown'}) + %} {% endif %} + {{ block('label') }} {% endblock %} diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/breadcrumbs.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/breadcrumbs.html.twig new file mode 100644 index 00000000000..321bf4430bb --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/breadcrumbs.html.twig @@ -0,0 +1,7 @@ + diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/pinbar.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/pinbar.html.twig index 01d39e3ac2f..bf7be778eb3 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/pinbar.html.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/Menu/pinbar.html.twig @@ -16,24 +16,16 @@
      {% if item.hasChildren and options.depth is not sameas(0) and item.displayChildren %} {% set pinbarItems = [] %} - {% set historyItems = {} %} {% for pinbarItem in item.children %} {% if pinbarItem.extras.isAllowed %} - {% if pinbarItem.extras.maximized %} - {% set historyItems = historyItems|merge({(pinbarItem.uri): pinbarItem.extras.maximized.format('U')}) %} - {% endif %} {% set pinbarItems = pinbarItems|merge([{'id': pinbarItem.extras.id, 'title': pinbarItem.label|raw, 'title_rendered': oro_title_render(pinbarItem.label|raw), 'url': pinbarItem.uri, 'type': pinbarItem.extras.type}]) %} {% endif %} {% endfor %} {% endif %} - {% if historyItems is defined %} - {% set historyItems = historyItems|sort %} - {% set options = options|merge({'history': historyItems|keys}) %} - {% endif %} {% import _self as pinbar %} - - +
      {% endif %} {% endblock %} diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Event/ResponseHashnavListenerTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Event/ResponseHashnavListenerTest.php index 2c80ef60c90..bf82d34769a 100644 --- a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Event/ResponseHashnavListenerTest.php +++ b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Event/ResponseHashnavListenerTest.php @@ -33,7 +33,7 @@ public function setUp() { $this->response = new Response(); $this->request = Request::create(self::TEST_URL); - $this->request->headers->add(array('x-oro-hash-navigation' => true)); + $this->request->headers->add(array(ResponseHashnavListener::HASH_NAVIGATION_HEADER => true)); $this->event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\FilterResponseEvent') ->disableOriginalConstructor() ->getMock(); diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Menu/BreadcrumbManagerTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Menu/BreadcrumbManagerTest.php new file mode 100644 index 00000000000..524030e5a5d --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Menu/BreadcrumbManagerTest.php @@ -0,0 +1,155 @@ +matcher = $this->getMockBuilder('Knp\Menu\Matcher\Matcher') + ->disableOriginalConstructor() + ->getMock(); + + $this->router = $this->getMockBuilder('Symfony\Component\Routing\Router') + ->disableOriginalConstructor() + ->getMock(); + + $this->provider = $this->getMockBuilder('Oro\Bundle\NavigationBundle\Provider\BuilderChainProvider') + ->disableOriginalConstructor() + ->getMock(); + + $this->factory = $this->getMockBuilder('Knp\Menu\MenuFactory') + ->setMethods(array('getRouteInfo', 'processRoute')) + ->getMock(); + + $this->manager = new BreadcrumbManager($this->provider, $this->matcher, $this->router); + } + + public function testGetBreadcrumbs() + { + $item = new MenuItem('test', $this->factory); + $subItem = new MenuItem('sub_item_test', $this->factory); + $subItem->setCurrent(true); + $item->addChild($subItem); + + $this->provider->expects($this->once()) + ->method('get') + ->with( + 'test', + array() + ) + ->will($this->returnValue($item)); + + $this->matcher->expects($this->any()) + ->method('isCurrent') + ->with($subItem) + ->will($this->returnValue(true)); + + + $breadcrumbs = $this->manager->getBreadcrumbs('test', false); + $this->assertEquals('sub_item_test', $breadcrumbs[0]['label']); + } + + public function testGetBreadcrumbsWOItem() + { + $item = new MenuItem('test', $this->factory); + + $this->provider->expects($this->once()) + ->method('get') + ->will($this->returnValue($item)); + $this->assertNull($this->manager->getBreadcrumbs('nullable')); + } + + public function testGetBreadcrumbLabels() + { + $item = new MenuItem('test', $this->factory); + $item->setExtra('routes', array( + 'another_route', + '/another_route/', + 'another*route', + 'test_route', + )); + $item1 = new MenuItem('test1', $this->factory); + $item2 = new MenuItem('sub_item', $this->factory); + $item1->addChild($item2); + $item1->setExtra('routes', array()); + $item2->addChild($item); + + + $this->provider->expects($this->once()) + ->method('get') + ->will($this->returnValue($item1)); + + $this->assertEquals( + array('test', 'sub_item', 'test1'), + $this->manager->getBreadcrumbLabels('test_menu', 'test_route') + ); + } + + public function testGetMenu() + { + $item = new MenuItem('testItem', $this->factory); + $subItem = new MenuItem('subItem', $this->factory); + $item->addChild($subItem); + $this->provider->expects($this->any()) + ->method('get') + ->will($this->returnValue($item)); + + $resultMenu = $this->manager->getMenu('test', array('subItem')); + $this->assertEquals($subItem, $resultMenu); + + $this->setExpectedException('InvalidArgumentException'); + $this->manager->getMenu('test', array('bad_item')); + } + + public function testGetCurrentMenuItem() + { + $item = new MenuItem('testItem', $this->factory); + $goodItem = new MenuItem('goodItem', $this->factory); + $subItem = new MenuItem('subItem', $this->factory); + $goodItem->addChild($subItem); + + $params = array( + 'testItem' => false, + 'goodItem' => false, + 'subItem' => true, + ); + + $this->matcher->expects($this->any()) + ->method('isCurrent') + ->with( + $this->logicalOr( + $this->equalTo($item), + $this->equalTo($goodItem), + $this->equalTo($subItem) + ) + ) + ->will( + $this->returnCallback( + function ($param) use (&$params) { + return $params[$param->getLabel()]; + } + ) + ); + + $this->assertEquals($subItem, $this->manager->getCurrentMenuItem(array($item, $goodItem))); + } +} diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Provider/TitleServiceTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Provider/TitleServiceTest.php index 20253f51b82..9db1e7c5f2c 100644 --- a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Provider/TitleServiceTest.php +++ b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Provider/TitleServiceTest.php @@ -35,6 +35,16 @@ class TitleServiceTest extends \PHPUnit_Framework_TestCase */ protected $repository; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $breadcrumbManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $userConfigManager; + /** * @var TitleService */ @@ -66,12 +76,22 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->userConfigManager = $this->getMockBuilder('Oro\Bundle\ConfigBundle\Config\UserConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->breadcrumbManager = $this->getMockBuilder('Oro\Bundle\NavigationBundle\Menu\BreadcrumbManager') + ->disableOriginalConstructor() + ->getMock(); + $this->titleService = new TitleService( $this->annotationsReader, $this->configReader, $this->translator, $this->em, - $this->serializer + $this->serializer, + $this->userConfigManager, + $this->breadcrumbManager ); } @@ -258,6 +278,13 @@ public function testUpdateItemsDuringUpdate() ->method('getRepository') ->will($this->returnValue($this->repository)); + $this->userConfigManager->expects($this->at(0))->method('get') + ->with('oro_navigation.breadcrumb_menu')->will($this->returnValue('test-menu')); + $this->userConfigManager->expects($this->at(1))->method('get') + ->with('oro_navigation.title_suffix')->will($this->returnValue('test-suffix')); + $this->userConfigManager->expects($this->at(2))->method('get') + ->with('oro_navigation.title_delimiter')->will($this->returnValue('/')); + $entityMock = $this->getMock('Oro\Bundle\NavigationBundle\Entity\Title'); $entityMock->expects($this->exactly(2)) @@ -270,7 +297,7 @@ public function testUpdateItemsDuringUpdate() $entityMock->expects($this->once()) ->method('setTitle') - ->with($this->equalTo('Title')); + ->with($this->equalTo('Title / test-breadcrumb / test-suffix')); $this->repository->expects($this->once()) ->method('findAll') @@ -282,6 +309,10 @@ public function testUpdateItemsDuringUpdate() $this->em->expects($this->once()) ->method('flush'); + $this->breadcrumbManager->expects($this->once()) + ->method('getBreadcrumbLabels') + ->will($this->returnValue(array('test-breadcrumb'))); + $this->titleService->update($testData); } diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/HashNavExtensionTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/HashNavExtensionTest.php index ab6b8e2ab6e..016099a59b2 100644 --- a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/HashNavExtensionTest.php +++ b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/HashNavExtensionTest.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\NavigationBundle\Tests\Unit\Twig; +use Oro\Bundle\NavigationBundle\Event\ResponseHashnavListener; use Oro\Bundle\NavigationBundle\Twig\HashNavExtension; class HashNavExtensionTest extends \PHPUnit_Framework_TestCase @@ -56,6 +57,15 @@ public function testGetFunctions() $functions = $this->extension->getFunctions(); $this->assertTrue(is_array($functions)); $this->assertTrue(array_key_exists('oro_is_hash_navigation', $functions)); + $this->assertTrue(array_key_exists('oro_hash_navigation_header', $functions)); + } + + public function testGetHashNavigationHeaderConst() + { + $this->assertEquals( + $this->extension->getHashNavigationHeaderConst(), + ResponseHashnavListener::HASH_NAVIGATION_HEADER + ); } public function testGetName() diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/MenuExtensionTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/MenuExtensionTest.php index f0d28e73c00..e1d7218af8f 100644 --- a/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/MenuExtensionTest.php +++ b/src/Oro/Bundle/NavigationBundle/Tests/Unit/Twig/MenuExtensionTest.php @@ -35,6 +35,11 @@ class MenuExtensionTest extends \PHPUnit_Framework_TestCase */ protected $factory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $breadcrumbManager; + /** * @var MenuExtension $menuExtension */ @@ -44,6 +49,10 @@ protected function setUp() { $this->container = new Container(); + $this->breadcrumbManager = $this->getMockBuilder('Oro\Bundle\NavigationBundle\Menu\BreadcrumbManager') + ->disableOriginalConstructor() + ->getMock(); + $this->helper = $this->getMockBuilder('Knp\Menu\Twig\Helper') ->disableOriginalConstructor() ->setMethods(array('render')) @@ -70,7 +79,7 @@ protected function setUp() $this->builder->setContainer($this->container); $provider->addBuilder($this->builder); - $this->menuExtension = new MenuExtension($this->helper, $provider, $this->container); + $this->menuExtension = new MenuExtension($this->helper, $provider, $this->breadcrumbManager, $this->container); } public function testGetFunctions() @@ -246,4 +255,50 @@ public function testBuildWithOptionsAndRenderer() $this->menuExtension ->render(array('navbar', 'user_user_show'), array('type' => 'some_menu'), 'some_renderer'); } + + public function testRenderBreadCrumbs() + { + $environment = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + + $template = $this->getMockBuilder('\Twig_TemplateInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->breadcrumbManager->expects($this->once()) + ->method('getBreadcrumbs') + ->will($this->returnValue(array('test-breadcrumb'))); + + $environment->expects($this->once()) + ->method('loadTemplate') + ->will($this->returnValue($template)); + + $template->expects($this->once()) + ->method('render') + ->with( + array( + 'breadcrumbs' => array( + 'test-breadcrumb' + ), + 'useDecorators' => true + ) + ); + ; + $this->menuExtension->renderBreadCrumbs($environment, 'test_menu'); + } + + public function testWrongBredcrumbs() + { + + $environment = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + + $this->breadcrumbManager->expects($this->once()) + ->method('getBreadcrumbs') + ->will($this->returnValue(null)); + + $this->assertNull($this->menuExtension->renderBreadCrumbs($environment, 'test_menu')); + } } diff --git a/src/Oro/Bundle/NavigationBundle/Twig/HashNavExtension.php b/src/Oro/Bundle/NavigationBundle/Twig/HashNavExtension.php index afffe2dc2ef..b82e2341beb 100644 --- a/src/Oro/Bundle/NavigationBundle/Twig/HashNavExtension.php +++ b/src/Oro/Bundle/NavigationBundle/Twig/HashNavExtension.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\NavigationBundle\Twig; +use Oro\Bundle\NavigationBundle\Event\ResponseHashnavListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernel; @@ -37,6 +38,11 @@ public function getFunctions() 'checkIsHashNavigation', array('is_safe' => array('html')) ), + 'oro_hash_navigation_header' => new \Twig_Function_Method( + $this, + 'getHashNavigationHeaderConst', + array('is_safe' => array('html')) + ), ); } @@ -49,12 +55,22 @@ public function checkIsHashNavigation() { return (!is_object($this->request) || ( - $this->request->headers->get('x-oro-hash-navigation') != true - && $this->request->get('x-oro-hash-navigation') != true + $this->request->headers->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER) != true + && $this->request->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER) != true ) ) ? false : true; } + /** + * Get hash navigation header string + * + * @return string + */ + public function getHashNavigationHeaderConst() + { + return ResponseHashnavListener::HASH_NAVIGATION_HEADER; + } + /** * Returns the name of the extension. * diff --git a/src/Oro/Bundle/NavigationBundle/Twig/MenuExtension.php b/src/Oro/Bundle/NavigationBundle/Twig/MenuExtension.php index fbf4b332235..79f2d31ef77 100644 --- a/src/Oro/Bundle/NavigationBundle/Twig/MenuExtension.php +++ b/src/Oro/Bundle/NavigationBundle/Twig/MenuExtension.php @@ -5,12 +5,17 @@ use Knp\Menu\ItemInterface; use Knp\Menu\Twig\Helper; use Knp\Menu\Provider\MenuProviderInterface; + +use Oro\Bundle\NavigationBundle\Menu\BreadcrumbManager; + use Symfony\Component\DependencyInjection\ContainerInterface; class MenuExtension extends \Twig_Extension { const MENU_NAME = 'oro_menu'; + const BREADCRUMBS_TEMPLATE = 'OroNavigationBundle:Menu:breadcrumbs.html.twig'; + /** * @var Helper $helper */ @@ -27,14 +32,25 @@ class MenuExtension extends \Twig_Extension protected $container; /** - * @param Helper $helper + * @var BreadcrumbManager + */ + protected $breadcrumbManager; + + /** + * @param Helper $helper * @param MenuProviderInterface $provider - * @param ContainerInterface $container + * @param BreadcrumbManager $breadcrumbManager + * @param ContainerInterface $container */ - public function __construct(Helper $helper, MenuProviderInterface $provider, ContainerInterface $container) - { + public function __construct( + Helper $helper, + MenuProviderInterface $provider, + BreadcrumbManager $breadcrumbManager, + ContainerInterface $container + ) { $this->helper = $helper; $this->provider = $provider; + $this->breadcrumbManager = $breadcrumbManager; $this->container = $container; } @@ -47,7 +63,15 @@ public function getFunctions() { return array( 'oro_menu_render' => new \Twig_Function_Method($this, 'render', array('is_safe' => array('html'))), - 'oro_menu_get' => new \Twig_Function_Method($this, 'getMenu') + 'oro_menu_get' => new \Twig_Function_Method($this, 'getMenu'), + 'oro_breadcrumbs' => new \Twig_Function_Method( + $this, + 'renderBreadCrumbs', + array( + 'is_safe' => array('html'), + 'needs_environment' => true + ) + ) ); } @@ -88,6 +112,30 @@ public function render($menu, array $options = array(), $renderer = null) return $this->helper->render($menu, $options, $renderer); } + /** + * Render breadcrumbs for menu + * + * @param \Twig_Environment $environment + * @param string $menuName + * @param bool $useDecorators + * @return null|string + */ + public function renderBreadCrumbs(\Twig_Environment $environment, $menuName, $useDecorators = true) + { + if ($breadcrumbs = $this->breadcrumbManager->getBreadcrumbs($menuName, $useDecorators)) { + $template = $environment->loadTemplate(self::BREADCRUMBS_TEMPLATE); + + return $template->render( + array( + 'breadcrumbs' => $breadcrumbs, + 'useDecorators' => $useDecorators + ) + ); + } + + return null; + } + /** * Returns the name of the extension. * diff --git a/src/Oro/Bundle/NotificationBundle/Command/SpoolSendCommand.php b/src/Oro/Bundle/NotificationBundle/Command/SpoolSendCommand.php new file mode 100644 index 00000000000..8f879b0821b --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Command/SpoolSendCommand.php @@ -0,0 +1,38 @@ +setName('oro:spool:send'); + $this->setHelp(str_replace('swiftmailer', 'oro', $this->getHelp())); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $mailer = $this->getContainer()->get('oro_notification.mailer'); + $this->getContainer()->set('mailer', $mailer); + + parent::execute($input, $output); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Controller/Api/Rest/EmailNotificationController.php b/src/Oro/Bundle/NotificationBundle/Controller/Api/Rest/EmailNotificationController.php new file mode 100644 index 00000000000..cf7ce30c3f1 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Controller/Api/Rest/EmailNotificationController.php @@ -0,0 +1,70 @@ +handleDeleteRequest($id); + } + + /** + * Get entity Manager + * + * @return ApiEntityManager + */ + public function getManager() + { + return $this->get('oro_notification.email_notification.manager.api'); + } + + /** + * @return FormInterface + */ + public function getForm() + { + return $this->get('oro_notification.form.type.email_notification.api'); + } + + /** + * @return ApiFormHandler + */ + public function getFormHandler() + { + return $this->get('oro_notification.form.handler.email_notification.api'); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Controller/EmailNotificationController.php b/src/Oro/Bundle/NotificationBundle/Controller/EmailNotificationController.php new file mode 100644 index 00000000000..e4c49e35d0b --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Controller/EmailNotificationController.php @@ -0,0 +1,99 @@ +get('oro_notification.emailnotification.datagrid_manager'); + $datagridView = $gridManager->getDatagrid()->createView(); + + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } + + return array('datagrid' => $datagridView); + } + + /** + * @Route("/update/{id}", requirements={"id"="\d+"}, defaults={"id"=0})) + * @Acl( + * id="oro_notification_emailnotification_update", + * name="Edit notification rule", + * description="Edit notification rule", + * parent="oro_notification_emailnotification" + * ) + * @Template() + */ + public function updateAction(EmailNotification $entity) + { + if ($this->get('oro_notification.form.handler.email_notification')->process($entity)) { + $this->get('session')->getFlashBag()->add( + 'success', + $this->get('translator')->trans('oro.notification.controller.emailnotification.saved.message') + ); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_notification_emailnotification_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_notification_emailnotification_index', + ) + ); + } + + return array( + 'form' => $this->get('oro_notification.form.email_notification')->createView(), + ); + } + + /** + * @Route("/create") + * @Acl( + * id="oro_notification_emailnotification_create", + * name="Create notification rule", + * description="Create notification rule", + * parent="oro_notification_emailnotification" + * ) + * @Template("OroNotificationBundle:EmailNotification:update.html.twig") + */ + public function createAction() + { + return $this->updateAction(new EmailNotification()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/DataFixtures/ORM/LoadDefaultNotificationEvents.php b/src/Oro/Bundle/NotificationBundle/DataFixtures/ORM/LoadDefaultNotificationEvents.php new file mode 100644 index 00000000000..a44e1c1f555 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/DataFixtures/ORM/LoadDefaultNotificationEvents.php @@ -0,0 +1,38 @@ + 'oro.notification.event.entity_post_update', + 'remove' => 'oro.notification.event.entity_post_remove', + 'create' => 'oro.notification.event.entity_post_persist' + ); + + foreach ($eventNames as $key => $name) { + $event = new Event($name, 'Event dispatched whenever any entity ' . $key . 's'); + $manager->persist($event); + } + $manager->flush(); + } + + /** + * {@inheritDoc} + */ + public function getOrder() + { + return 120; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Datagrid/EmailNotificationDatagridManager.php b/src/Oro/Bundle/NotificationBundle/Datagrid/EmailNotificationDatagridManager.php new file mode 100644 index 00000000000..1fcd575beb5 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Datagrid/EmailNotificationDatagridManager.php @@ -0,0 +1,292 @@ +entityNameChoise = array_map( + function ($value) { + return isset($value['name'])? $value['name'] : ''; + }, + $entitiesConfig + ); + } + + /** + * {@inheritDoc} + */ + protected function getProperties() + { + return array( + new UrlProperty('update_link', $this->router, 'oro_notification_emailnotification_update', array('id')), + new UrlProperty('delete_link', $this->router, 'oro_api_delete_emailnotication', array('id')), + ); + } + + /** + * {@inheritDoc} + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $fieldId = new FieldDescription(); + $fieldId->setName('id'); + $fieldId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => $this->translate('ID'), + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'show_column' => false + ) + ); + $fieldsCollection->add($fieldId); + + $fieldEntityName = new FieldDescription(); + $fieldEntityName->setName('entityName'); + $fieldEntityName->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => $this->translate('oro.notification.datagrid.entity_name'), + 'field_name' => 'entityName', + 'filter_type' => FilterInterface::TYPE_CHOICE, + 'choices' => $this->entityNameChoise, + 'translation_domain' => 'config', + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $templateDataProperty = new TwigTemplateProperty( + $fieldEntityName, + 'OroNotificationBundle:EmailNotification:Datagrid/Property/entityName.html.twig' + ); + $fieldEntityName->setProperty($templateDataProperty); + $fieldsCollection->add($fieldEntityName); + + $fieldEvent = new FieldDescription(); + $fieldEvent->setName('event'); + $fieldEvent->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_OPTIONS, + 'label' => $this->translate('oro.notification.datagrid.event_name'), + 'field_name' => 'eventName', + 'expression' => 'event', + 'filter_type' => FilterInterface::TYPE_ENTITY, + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + // entity filter options + 'multiple' => true, + 'class' => 'OroNotificationBundle:Event', + 'property' => 'name', + 'filter_by_where' => true, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('e'); + }, + ) + ); + + $property = new TranslateableProperty('event', $this->translator, 'eventName'); + $fieldEvent->setProperty($property); + $fieldsCollection->add($fieldEvent); + + $fieldTemplate = new FieldDescription(); + $fieldTemplate->setName('template'); + $fieldTemplate->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => $this->translate('oro.notification.datagrid.template'), + 'filter_type' => FilterInterface::TYPE_STRING, + 'expression' => 'template.name', + 'field_name' => 'template', + 'filter_by_where' => true, + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldTemplate); + + // Recipient filters + $fieldRecipientList = new FieldDescription(); + $fieldRecipientList->setName('recipientUsersList'); + $fieldRecipientList->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_HTML, + 'field_name' => 'recipientUsersList', + 'expression' => 'recipientUsersList', + 'label' => $this->translate('oro.notification.datagrid.recipient.user'), + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + // entity filter options + 'multiple' => true, + 'filter_type' => FilterInterface::TYPE_ENTITY, + 'class' => 'OroUserBundle:User', + 'property' => 'fullName', + 'filter_by_where' => true, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('e'); + }, + ) + ); + $templateDataProperty = new TwigTemplateProperty( + $fieldRecipientList, + 'OroNotificationBundle:EmailNotification:Datagrid/Property/recipientList.html.twig' + ); + $fieldRecipientList->setProperty($templateDataProperty); + $fieldsCollection->add($fieldRecipientList); + + $fieldRecipientList = new FieldDescription(); + $fieldRecipientList->setName('recipientGroupsList'); + $fieldRecipientList->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_HTML, + 'field_name' => 'recipientGroupsList', + 'expression' => 'recipientGroupsList', + 'label' => $this->translate('oro.notification.datagrid.recipient.group'), + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + // entity filter options + 'multiple' => true, + 'filter_type' => FilterInterface::TYPE_ENTITY, + 'class' => 'OroUserBundle:Group', + 'property' => 'name', + 'filter_by_where' => true, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('e'); + }, + ) + ); + $templateDataProperty = new TwigTemplateProperty( + $fieldRecipientList, + 'OroNotificationBundle:EmailNotification:Datagrid/Property/recipientList.html.twig' + ); + $fieldRecipientList->setProperty($templateDataProperty); + $fieldsCollection->add($fieldRecipientList); + + $fieldRecipientList = new FieldDescription(); + $fieldRecipientList->setName('emailRecipient'); + $fieldRecipientList->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'field_name' => 'emailRecipient', + 'expression' => 'recipientList.email', + 'label' => $this->translate('oro.notification.datagrid.recipient.custom_email'), + 'required' => false, + 'sortable' => false, + 'show_filter' => true, + 'filterable' => true, + 'filter_by_having' => true, + 'filter_type' => FilterInterface::TYPE_STRING + ) + ); + $fieldsCollection->add($fieldRecipientList); + + $fieldRecipientList = new FieldDescription(); + $fieldRecipientList->setName('ownerRecipient'); + $fieldRecipientList->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_BOOLEAN, + 'field_name' => 'ownerRecipient', + 'expression' => 'recipientList.owner', + 'label' => $this->translate('oro.notification.datagrid.recipient.owner'), + 'required' => false, + 'sortable' => false, + 'filterable' => true, + 'show_filter' => true, + 'filter_by_having' => true, + 'filter_type' => FilterInterface::TYPE_BOOLEAN + ) + ); + $fieldsCollection->add($fieldRecipientList); + } + + /** + * {@inheritDoc} + */ + protected function getRowActions() + { + $clickAction = array( + 'name' => 'rowClick', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_notification_emailnotification_update', + 'options' => array( + 'label' => $this->translate('oro.notification.datagrid.action.update'), + 'link' => 'update_link', + 'runOnRowClick' => true, + ) + ); + + $updateAction = array( + 'name' => 'update', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_notification_emailnotification_update', + 'options' => array( + 'label' => $this->translate('oro.notification.datagrid.action.update'), + 'icon' => 'edit', + 'link' => 'update_link', + ) + ); + + $deleteAction = array( + 'name' => 'delete', + 'type' => ActionInterface::TYPE_DELETE, + 'acl_resource' => 'oro_notification_emailnotification_remove', + 'options' => array( + 'label' => $this->translate('oro.notification.datagrid.action.delete'), + 'icon' => 'trash', + 'link' => 'delete_link', + ) + ); + + return array($clickAction, $updateAction, $deleteAction); + } + + /** + * {@inheritDoc} + */ + protected function prepareQuery(ProxyQueryInterface $query) + { + $entityAlias = $query->getRootAlias(); + + /** @var $query QueryBuilder */ + $query->leftJoin($entityAlias . '.event', 'event'); + $query->leftJoin($entityAlias . '.template', 'template'); + $query->leftJoin($entityAlias . '.recipientList', 'recipientList'); + $query->leftJoin('recipientList.users', 'recipientUsersList'); + $query->leftJoin('recipientList.groups', 'recipientGroupsList'); + + $query->addSelect('event.name as eventName', true); + $query->addSelect('recipientList.owner as ownerRecipient', true); + $query->addSelect('recipientList.email as emailRecipient', true); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php new file mode 100644 index 00000000000..1ac2d3e095b --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php @@ -0,0 +1,56 @@ +hasDefinition(self::SERVICE_KEY)) { + return; + } + + $eventClassName = $container->getParameter('oro_notification.event_entity.class'); + + $dispatcher = $container->getDefinition(self::DISPATCHER_KEY); + $em = $container->get('doctrine.orm.entity_manager'); + + $eventNames = array(); + if ($this->isSchemaSynced($em, $eventClassName) !== false) { + $eventNames = $em->getRepository($eventClassName) + ->getEventNames(); + } + foreach ($eventNames as $eventName) { + $dispatcher->addMethodCall( + 'addListenerService', + array($eventName['name'], array(self::SERVICE_KEY, 'process')) + ); + } + } + + /** + * Returns false if database schema is not created correctly + * + * @param EntityManager $em + * @param string $className + * @return int|bool + */ + protected function isSchemaSynced(EntityManager $em, $className) + { + $tables = $em->getConnection()->getSchemaManager()->listTableNames(); + $table = $em->getClassMetadata($className)->getTableName(); + + return array_search($table, $tables); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/NotificationHandlerPass.php b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/NotificationHandlerPass.php new file mode 100644 index 00000000000..7d6a82de72e --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/NotificationHandlerPass.php @@ -0,0 +1,30 @@ +hasDefinition(self::SERVICE_KEY)) { + return; + } + + $serviceDefinition = $container->getDefinition(self::SERVICE_KEY); + $taggedServices = $container->findTaggedServiceIds(self::TAG); + + foreach ($taggedServices as $serviceId => $taggedService) { + $serviceDefinition->addMethodCall('addHandler', array(new Reference($serviceId))); + } + } +} diff --git a/src/Oro/Bundle/NotificationBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..190c07d3c16 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Configuration.php @@ -0,0 +1,26 @@ +root('oro_notification'); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/DependencyInjection/OroNotificationExtension.php b/src/Oro/Bundle/NotificationBundle/DependencyInjection/OroNotificationExtension.php new file mode 100644 index 00000000000..cca3b0d9d8a --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/DependencyInjection/OroNotificationExtension.php @@ -0,0 +1,29 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + $loader->load('datagrid.yml'); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/EmailNotification.php b/src/Oro/Bundle/NotificationBundle/Entity/EmailNotification.php new file mode 100644 index 00000000000..5305ea3aa2a --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/EmailNotification.php @@ -0,0 +1,204 @@ +id; + } + + /** + * Set entityName + * + * @param string $entityName + * @return EmailNotification + */ + public function setEntityName($entityName) + { + $this->entityName = $entityName; + + return $this; + } + + /** + * Get entityName + * + * @return string + */ + public function getEntityName() + { + return $this->entityName; + } + + /** + * Set event + * + * @param Event $event + * @return EmailNotification + */ + public function setEvent(Event $event) + { + $this->event = $event; + + return $this; + } + + /** + * Get event + * + * @return Event + */ + public function getEvent() + { + return $this->event; + } + + /** + * Set template + * + * @param EmailTemplate $template + * @return EmailNotification + */ + public function setTemplate(EmailTemplate $template) + { + $this->template = $template; + + return $this; + } + + /** + * Get template + * + * @return EmailTemplate + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Set recipient + * + * @param RecipientList $recipientList + * @return EmailNotification + */ + public function setRecipientList(RecipientList $recipientList) + { + $this->recipientList = $recipientList; + + return $this; + } + + /** + * Get recipient + * + * @return RecipientList + */ + public function getRecipientList() + { + return $this->recipientList; + } + + /** + * Returns comma separated list + * + * @return string + */ + public function getRecipientGroupsList() + { + if (!$this->getRecipientList()) { + return ''; + } + + return implode( + ', ', + $this->getRecipientList()->getGroups()->map( + function (Group $group) { + return $group->getName(); + } + )->toArray() + ); + } + + /** + * Returns comma separated list + * + * @return string + */ + public function getRecipientUsersList() + { + if (!$this->getRecipientList()) { + return ''; + } + + return implode( + ', ', + $this->getRecipientList()->getUsers()->map( + function (User $user) { + return sprintf('%s <%s>', $user->getFullname(), $user->getEmail()); + } + )->toArray() + ); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/Event.php b/src/Oro/Bundle/NotificationBundle/Entity/Event.php new file mode 100644 index 00000000000..05cf5070828 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/Event.php @@ -0,0 +1,101 @@ +name = $name; + $this->description = $description; + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set name + * + * @param string $name + * @return Event + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set description + * + * @param string $description + * @return Event + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Get description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/RecipientList.php b/src/Oro/Bundle/NotificationBundle/Entity/RecipientList.php new file mode 100644 index 00000000000..f888e549348 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/RecipientList.php @@ -0,0 +1,260 @@ +groups = new ArrayCollection(); + $this->users = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * Setter for email + * + * @param string $email + */ + public function setEmail($email) + { + $this->email = $email; + } + + /** + * Getter for email + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Gets the groups related to list + * + * @return ArrayCollection + */ + public function getGroups() + { + return $this->groups; + } + + /** + * Add specified group + * + * @param Group $group + * @return $this + */ + public function addGroup(Group $group) + { + if (!$this->getGroups()->contains($group)) { + $this->getGroups()->add($group); + } + + return $this; + } + + /** + * Remove specified group + * + * @param Group $group + * @return $this + */ + public function removeGroup(Group $group) + { + if ($this->getGroups()->contains($group)) { + $this->getGroups()->removeElement($group); + } + + return $this; + } + + /** + * Add specified user + * + * @param User $user + * @return $this + */ + public function addUser(User $user) + { + if (!$this->getUsers()->contains($user)) { + $this->getUsers()->add($user); + } + + return $this; + } + + /** + * Remove specified user + * + * @param User $user + * @return $this + */ + public function removeUser(User $user) + { + if ($this->getUsers()->contains($user)) { + $this->getUsers()->removeElement($user); + } + + return $this; + } + + /** + * Getters for users + * + * @return ArrayCollection + */ + public function getUsers() + { + return $this->users; + } + + /** + * Setter for owner field + * + * @param boolean $owner + */ + public function setOwner($owner) + { + $this->owner = $owner; + } + + /** + * Getter for owner field + * + * @return boolean + */ + public function getOwner() + { + return $this->owner; + } + + /** + * To string implementation + * + * @return string + */ + public function __toString() + { + // get user emails + $results = $this->getUsers()->map( + function (User $user) { + return sprintf( + '%s %s <%s>', + $user->getFirstName(), + $user->getLastName(), + $user->getEmail() + ); + } + )->toArray(); + + $results = array_merge( + $results, + $this->getGroups()->map( + function (Group $group) use (&$results) { + return sprintf( + '%s (group)', + $group->getName() + ); + } + )->toArray() + ); + + if ($this->getEmail()) { + $results[] = sprintf('Custom email: <%s>', $this->getEmail()); + } + + if ($this->getOwner()) { + $results[] = 'Entity owner'; + } + + return implode(', ', $results); + } + + /** + * Custom validation constraint + * Not valid if no one recipient specified + * + * @param ExecutionContext $context + */ + public function isValid(ExecutionContext $context) + { + $notValid = + $this->getGroups()->isEmpty() + && $this->getUsers()->isEmpty() + && $this->getEmail() == null + && $this->getOwner() == null; + + if ($notValid) { + $propertyPath = $context->getPropertyPath() . '.recipientList'; + $context->addViolationAt( + $propertyPath, + 'oro.notification.validators.recipient_list.empty.message' + ); + } + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/Repository/EmailNotificationRepository.php b/src/Oro/Bundle/NotificationBundle/Entity/Repository/EmailNotificationRepository.php new file mode 100644 index 00000000000..2bf31cf1b70 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/Repository/EmailNotificationRepository.php @@ -0,0 +1,26 @@ +createQueryBuilder('emn') + ->select(array('emn', 'event')) + ->leftJoin('emn.event', 'event') + ->getQuery() + ->getResult(); + + return new ArrayCollection($rules); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/Repository/EventRepository.php b/src/Oro/Bundle/NotificationBundle/Entity/Repository/EventRepository.php new file mode 100644 index 00000000000..187d75edb8b --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/Repository/EventRepository.php @@ -0,0 +1,19 @@ +createQueryBuilder('e') + ->select('e.name') + ->getQuery() + ->getResult(); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/Repository/RecipientListRepository.php b/src/Oro/Bundle/NotificationBundle/Entity/Repository/RecipientListRepository.php new file mode 100644 index 00000000000..6ffcb3961f2 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/Repository/RecipientListRepository.php @@ -0,0 +1,63 @@ +getUsers()->map( + function ($user) { + return $user->getEmail(); + } + ); + + $groupIds = $recipientList->getGroups()->map( + function ($group) { + return $group->getId(); + } + )->toArray(); + + if ($groupIds) { + $groupUsers = $this->_em->createQueryBuilder() + ->select('u.email') + ->from('OroUserBundle:User', 'u') + ->leftJoin('u.groups', 'groups') + ->where('groups.id IN (:groupIds)') + ->setParameter('groupIds', $groupIds) + ->getQuery() + ->getResult(); + + // add group users emails + array_map( + function ($groupEmail) use ($emails) { + $emails[] = $groupEmail['email']; + }, + $groupUsers + ); + } + + // add owner email + if ($recipientList->getOwner() && $entity instanceof ContainAuthorInterface) { + $emails[] = $entity->getCreatedBy()->getEmail(); + } + + // add custom email + if ($recipientList->getEmail()) { + $emails[] = $recipientList->getEmail(); + } + + return array_unique($emails->toArray()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Entity/Repository/SpoolItemRepository.php b/src/Oro/Bundle/NotificationBundle/Entity/Repository/SpoolItemRepository.php new file mode 100644 index 00000000000..2f6d5950729 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Entity/Repository/SpoolItemRepository.php @@ -0,0 +1,15 @@ +id; + } + + /** + * Set status + * + * @param string $status + * @return SpoolItem + */ + public function setStatus($status) + { + $this->status = $status; + + return $this; + } + + /** + * Get status + * + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * Set message + * + * @param string $message + * @return SpoolItem + */ + public function setMessage($message) + { + $this->message = $message; + + return $this; + } + + /** + * Get message + * + * @return string + */ + public function getMessage() + { + return $this->message; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Event/Handler/EmailNotificationHandler.php b/src/Oro/Bundle/NotificationBundle/Event/Handler/EmailNotificationHandler.php new file mode 100644 index 00000000000..3bc583cf250 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Event/Handler/EmailNotificationHandler.php @@ -0,0 +1,208 @@ +twig = $twig; + $this->mailer = $mailer; + $this->em = $em; + $this->sendFrom = $sendFrom; + $this->logger = $logger; + + $this->user = $this->getUser($securityContext); + } + + /** + * Handle event + * + * @param NotificationEvent $event + * @param EmailNotification[] $matchedNotifications + * @return mixed + */ + public function handle(NotificationEvent $event, $matchedNotifications) + { + $entity = $event->getEntity(); + + foreach ($matchedNotifications as $notification) { + $emailTemplate = $notification->getTemplate(); + $templateParams = array( + 'event' => $event, + 'notification' => $notification, + 'entity' => $entity, + 'templateName' => $emailTemplate, + 'user' => $this->user, + ); + + $recipientEmails = $this->em->getRepository('Oro\Bundle\NotificationBundle\Entity\RecipientList') + ->getRecipientEmails($notification->getRecipientList(), $entity); + + $content = $emailTemplate->getContent(); + // ensure we have no html tags in txt template + $content = $emailTemplate->getType() == 'txt' ? strip_tags($content) : $content; + + try { + $templateRendered = $this->twig->render($content, $templateParams); + $subjectRendered = $this->twig->render($emailTemplate->getSubject(), $templateParams); + } catch (\Twig_Error $e) { + $templateRendered = false; + $subjectRendered = false; + + $this->logger->log( + Logger::ERROR, + sprintf( + 'Error rendering email template (id: %d), %s', + $emailTemplate->getId(), + $e->getMessage() + ) + ); + } + + if ($templateRendered === false || $subjectRendered === false) { + break; + } + + // TODO: use locale for subject and body + $params = new ParameterBag( + array( + 'subject' => $subjectRendered, + 'body' => $templateRendered, + 'from' => $this->sendFrom, + 'to' => $recipientEmails, + 'type' => $emailTemplate->getType() == 'txt' ? 'text/plain' : 'text/html' + ) + ); + + $this->notify($params); + $this->addJob(self::SEND_COMMAND); + } + } + + /** + * {@inheritdoc} + */ + public function notify(ParameterBag $params) + { + $recipients = $params->get('to'); + if (empty($recipients)) { + return false; + } + + foreach ($recipients as $email) { + $message = \Swift_Message::newInstance() + ->setSubject($params->get('subject')) + ->setFrom($params->get('from')) + ->setTo($email) + ->setBody($params->get('body'), $params->get('type')); + $this->mailer->send($message); + } + + return true; + } + + /** + * Add swiftmailer spool send task to job queue if it has not been added earlier + * + * @param string $command + * @param array $commandArgs + * @return boolean|integer + */ + public function addJob($command, $commandArgs = array()) + { + $commandArgs = array_merge( + array( + 'message-limit' => $this->messageLimit, + 'env' => $this->env, + ), + $commandArgs + ); + + if ($commandArgs['env'] == 'prod') { + $commandArgs['no-debug'] = true; + } + + return parent::addJob($command, $commandArgs); + } + + /** + * Set message limit + * + * @param int $messageLimit + */ + public function setMessageLimit($messageLimit) + { + $this->messageLimit = $messageLimit; + } + + /** + * Set environment + * + * @param string $env + */ + public function setEnv($env) + { + $this->env = $env; + } + + /** + * Return current user + * + * @param SecurityContextInterface $securityContext + * @return User|bool + */ + private function getUser(SecurityContextInterface $securityContext) + { + return $securityContext->getToken() && !is_string($securityContext->getToken()->getUser()) + ? $securityContext->getToken()->getUser() : false; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerAbstract.php b/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerAbstract.php new file mode 100644 index 00000000000..8fe49bbad3c --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerAbstract.php @@ -0,0 +1,38 @@ +em + ->createQuery("SELECT j FROM JMSJobQueueBundle:Job j WHERE j.command = :command AND j.state <> :state") + ->setParameter('command', $command) + ->setParameter('state', Job::STATE_FINISHED) + ->setMaxResults(1) + ->getOneOrNullResult(); + + if (!$currJob) { + $job = new Job($command, $commandArgs); + $this->em->persist($job); + $this->em->flush($job); + } + + return $currJob ? $currJob->getId(): true; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerInterface.php b/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerInterface.php new file mode 100644 index 00000000000..f3b8a185a54 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Event/Handler/EventHandlerInterface.php @@ -0,0 +1,27 @@ +entity = $entity; + } + + /** + * Set entity + * + * @param $entity + */ + public function setEntity($entity) + { + $this->entity = $entity; + } + + /** + * Get entity + * + * @return mixed + */ + public function getEntity() + { + return $this->entity; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Form/Handler/EmailNotificationHandler.php b/src/Oro/Bundle/NotificationBundle/Form/Handler/EmailNotificationHandler.php new file mode 100644 index 00000000000..45ad9f20ac2 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Form/Handler/EmailNotificationHandler.php @@ -0,0 +1,64 @@ +form = $form; + $this->request = $request; + $this->manager = $manager; + } + + /** + * Process form + * + * @param EmailNotification $entity + * @return bool True on successfull processing, false otherwise + */ + public function process(EmailNotification $entity) + { + $this->form->setData($entity); + + if (in_array($this->request->getMethod(), array('POST', 'PUT'))) { + $this->form->submit($this->request); + + if ($this->form->isValid()) { + $this->manager->persist($entity); + $this->manager->flush(); + + return true; + } + } + + return false; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationApiType.php b/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationApiType.php new file mode 100644 index 00000000000..8a47c40fd58 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationApiType.php @@ -0,0 +1,42 @@ +addEventSubscriber(new PatchSubscriber()); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\NotificationBundle\Entity\EmailNotification', + 'intention' => 'emailnotification', + 'csrf_protection' => false, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'emailnotification_api'; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationType.php b/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationType.php new file mode 100644 index 00000000000..7fa0d16f731 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Form/Type/EmailNotificationType.php @@ -0,0 +1,137 @@ +subscriber = $subscriber; + $this->entityNameChoices = array_map( + function ($value) { + return isset($value['name']) ? $value['name'] : ''; + }, + $entitiesConfig + ); + + $this->entitiesData = $entitiesConfig; + array_walk( + $this->entitiesData, + function (&$value, $key) { + $reflection = new \ReflectionClass($key); + $interfaces = $reflection->getInterfaceNames(); + + /** + * @TODO change interface name when entityConfigBundle will provide responsibility of owner interface + */ + $value = array_search('Oro\\Bundle\\TagBundle\\Entity\\ContainAuthorInterface', $interfaces) !== false; + } + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->subscriber); + + $builder->add( + 'entityName', + 'choice', + array( + 'choices' => $this->entityNameChoices, + 'multiple' => false, + 'translation_domain' => 'config', + 'empty_value' => '', + 'empty_data' => null, + 'required' => true, + 'attr' => array( + 'data-entities' => json_encode($this->entitiesData) + ) + ) + ); + + $builder->add( + 'event', + 'entity', + array( + 'class' => 'OroNotificationBundle:Event', + 'property' => 'name', + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('c') + ->orderBy('c.name', 'ASC'); + }, + 'empty_value' => '', + 'empty_data' => null, + 'required' => true + ) + ); + + $builder->add( + 'template', + 'oro_email_template_list', + array( + 'required' => true + ) + ); + + $builder->add( + 'recipientList', + 'oro_notification_recipient_list', + array( + 'required' => true, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\NotificationBundle\Entity\EmailNotification', + 'intention' => 'emailnotification', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'cascade_validation' => true, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'emailnotification'; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Form/Type/RecipientListType.php b/src/Oro/Bundle/NotificationBundle/Form/Type/RecipientListType.php new file mode 100644 index 00000000000..f0454d269f2 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Form/Type/RecipientListType.php @@ -0,0 +1,92 @@ +entityManager = $entityManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add( + 'users', + 'oro_user_multiselect', + array( + 'required' => false + ) + ); + + // groups + $builder->add( + 'groups', + 'entity', + array( + 'class' => 'OroUserBundle:Group', + 'property' => 'name', + 'multiple' => true, + 'expanded' => true, + 'empty_value' => '', + 'empty_data' => null, + 'required' => false, + ) + ); + + // custom email + $builder->add( + 'email', + 'email', + array( + 'required' => false + ) + ); + + // owner + $builder->add( + 'owner', + 'checkbox', + array( + 'required' => false + ) + ); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\NotificationBundle\Entity\RecipientList', + 'intention' => 'recipientlist', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'cascade_validation' => true, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_notification_recipient_list'; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/OroNotificationBundle.php b/src/Oro/Bundle/NotificationBundle/OroNotificationBundle.php new file mode 100644 index 00000000000..5c82e742eab --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/OroNotificationBundle.php @@ -0,0 +1,42 @@ +kernel = $kernel; + } + + /** + * Builds the bundle. + * + * It is only ever called once when the cache is empty. + * + * This method can be overridden to register compilation passes, + * other extensions, ... + * + * @param ContainerBuilder $container A ContainerBuilder instance + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new NotificationHandlerPass()) + ->addCompilerPass(new EventsCompilerPass(), PassConfig::TYPE_AFTER_REMOVING); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Provider/DoctrineListener.php b/src/Oro/Bundle/NotificationBundle/Provider/DoctrineListener.php new file mode 100644 index 00000000000..095b0b900a7 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Provider/DoctrineListener.php @@ -0,0 +1,101 @@ +getEventDispatcher() + ->dispatch('oro.notification.event.entity_post_update', $this->getNotificationEvent($args)); + + return $this; + } + + /** + * Post persist event process + * + * @param LifecycleEventArgs $args + * @return $this + */ + public function postPersist(LifecycleEventArgs $args) + { + $this->getEventDispatcher() + ->dispatch('oro.notification.event.entity_post_persist', $this->getNotificationEvent($args)); + + return $this; + } + + /** + * Post remove event process + * + * @param LifecycleEventArgs $args + * @return $this + */ + public function postRemove(LifecycleEventArgs $args) + { + $this->getEventDispatcher() + ->dispatch('oro.notification.event.entity_post_remove', $this->getNotificationEvent($args)); + + return $this; + } + + /** + * Create new event instance + * + * @param LifecycleEventArgs $args + * @return NotificationEvent + */ + public function getNotificationEvent(LifecycleEventArgs $args) + { + $event = new NotificationEvent($args->getEntity()); + + return $event; + } + + /** + * Getter for event dispatcher object + * + * @return EventDispatcherInterface + */ + public function getEventDispatcher() + { + return $this->eventDispatcher; + } + + /** + * Setter for event dispatcher object + * + * @param EventDispatcherInterface $eventDispatcher + * @return $this + */ + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + + return $this; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Provider/Mailer/DbSpool.php b/src/Oro/Bundle/NotificationBundle/Provider/Mailer/DbSpool.php new file mode 100644 index 00000000000..d899e5b10f9 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Provider/Mailer/DbSpool.php @@ -0,0 +1,123 @@ +em = $em; + $this->entityClass = $entityClass; + } + + /** + * Starts this Spool mechanism. + */ + public function start() + { + } + + /** + * Stops this Spool mechanism. + */ + public function stop() + { + } + + /** + * Tests if this Spool mechanism has started. + * + * @return boolean + */ + public function isStarted() + { + return true; + } + + /** + * Queues a message. + * + * @param \Swift_Mime_Message $message The message to store + * @return boolean Whether the operation has succeeded + * @throws \Swift_IoException if the persist fails + */ + public function queueMessage(\Swift_Mime_Message $message) + { + /** @var SpoolItem $mailObject */ + $mailObject = new $this->entityClass; + $mailObject->setMessage(serialize($message)); + $mailObject->setStatus(self::STATUS_READY); + + try { + $this->em->persist($mailObject); + $this->em->flush($mailObject); + } catch (\Exception $e) { + throw new \Swift_IoException("Unable to persist object for enqueuing message"); + } + + return true; + } + + /** + * Sends messages using the given transport instance. + * + * @param \Swift_Transport $transport A transport instance + * @param string[] &$failedRecipients An array of failures by-reference + * + * @return int The number of sent emails + */ + public function flushQueue(\Swift_Transport $transport, &$failedRecipients = null) + { + if (!$transport->isStarted()) { + $transport->start(); + } + + $repo = $this->em->getRepository($this->entityClass); + $limit = $this->getMessageLimit(); + $limit = $limit > 0 ? $limit : null; + $emails = $repo->findBy(array("status" => self::STATUS_READY), null, $limit); + if (!count($emails)) { + return 0; + } + + $failedRecipients = (array) $failedRecipients; + $count = 0; + $time = time(); + /** @var SpoolItem $email */ + foreach ($emails as $email) { + $email->setStatus(self::STATUS_PROCESSING); + $this->em->persist($email); + $this->em->flush(); + + /** @var \Swift_Message $message */ + $message = unserialize($email->getMessage()); + $count += $transport->send($message, $failedRecipients); + $this->em->remove($email); + $this->em->flush(); + + if ($this->getTimeLimit() && (time() - $time) >= $this->getTimeLimit()) { + break; + } + } + + return $count; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Provider/NotificationManager.php b/src/Oro/Bundle/NotificationBundle/Provider/NotificationManager.php new file mode 100644 index 00000000000..1d8ec4c74ab --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Provider/NotificationManager.php @@ -0,0 +1,105 @@ +handlers = array(); + $this->em = $em; + $this->className = $className; + $this->notificationRules = $this->em->getRepository($this->className) + ->getRules(); + } + + /** + * Add handler to list + * + * @param EventHandlerInterface $handler + */ + public function addHandler(EventHandlerInterface $handler) + { + $this->handlers[] = $handler; + } + + /** + * Process events with handlers + * + * @param NotificationEvent $event + * @return NotificationEvent + */ + public function process(NotificationEvent $event) + { + $entity = $event->getEntity(); + + // select rules by entity name and event name + $notificationRules = $this->getRulesByCriteria(get_class($entity), $event->getName()); + + if (!$notificationRules->isEmpty()) { + /** @var EventHandlerInterface $handler */ + foreach ($this->handlers as $handler) { + $handler->handle($event, $notificationRules); + + if ($event->isPropagationStopped()) { + break; + } + } + } + + return $event; + } + + /** + * Return list of handlers + * + * @return EventHandlerInterface[] + */ + public function getHandlers() + { + return $this->handlers; + } + + /** + * Filter rules by criteria + * + * @param string $entityName + * @param string $eventName + * @return \Doctrine\Common\Collections\Collection|ArrayCollection + */ + protected function getRulesByCriteria($entityName, $eventName) + { + return $this->notificationRules->filter( + function (EmailNotification $item) use ($entityName, $eventName) { + return $item->getEntityName() == $entityName + && $item->getEvent()->getName() == $eventName; + } + ); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml new file mode 100644 index 00000000000..a372584cdee --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml @@ -0,0 +1,13 @@ +parameters: + oro_notification.emailnotification.datagrid_manager.class: Oro\Bundle\NotificationBundle\Datagrid\EmailNotificationDatagridManager + +services: + oro_notification.emailnotification.datagrid_manager: + class: %oro_notification.emailnotification.datagrid_manager.class% + arguments: [%oro_config.entities%] + tags: + - name: oro_grid.datagrid.manager + datagrid_name: emailnotification + entity_name: %oro_notification.emailnotification.entity.class% + entity_hint: transactional email + route_name: oro_notification_emailnotification_index diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/navigation.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/navigation.yml new file mode 100644 index 00000000000..c7342b5dd5e --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/navigation.yml @@ -0,0 +1,18 @@ +oro_menu_config: + items: + oro_notification_emailnotification_list: + label: 'Transaction Emails' + route: 'oro_notification_emailnotification_index' + extras: + routes: ['oro_notification_emailnotification_*'] + tree: + application_menu: + children: + system_tab: + children: + oro_notification_emailnotification_list: ~ + +oro_titles: + oro_notification_emailnotification_index: ~ + oro_notification_emailnotification_update: "Edit Notification Rule #%%id%%" + oro_notification_emailnotification_create: "Add Notification Rule" diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/routing.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/routing.yml new file mode 100644 index 00000000000..15854e50973 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/routing.yml @@ -0,0 +1,13 @@ +oro_notification: + resource: "@OroNotificationBundle/Controller" + type: annotation + prefix: /notification + + +oro_notification_api_emailnotification: + resource: "@OroNotificationBundle/Controller/Api/Rest/EmailNotificationController.php" + type: rest + prefix: api/rest/{version}/ + defaults: + version: latest + diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/services.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/services.yml new file mode 100644 index 00000000000..aebb8bc04b3 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/services.yml @@ -0,0 +1,175 @@ +parameters: + oro_notification.send_from: example@example.com + + # Entity classes + oro_notification.event_entity.class: Oro\Bundle\NotificationBundle\Entity\Event + oro_notification.emailnotification.entity.class: Oro\Bundle\NotificationBundle\Entity\EmailNotification + + # Event manager and event handler classes + oro_notification.manager.class: Oro\Bundle\NotificationBundle\Provider\NotificationManager + oro_notification.email_handler.class: Oro\Bundle\NotificationBundle\Event\Handler\EmailNotificationHandler + + # Listener classes + oro_notification.doctrine_listener.class: Oro\Bundle\NotificationBundle\Provider\DoctrineListener + + # Email notification form services + oro_notification.form.type.email_notification.class: Oro\Bundle\NotificationBundle\Form\Type\EmailNotificationType + oro_notification.form.type.recipient_list.class: Oro\Bundle\NotificationBundle\Form\Type\RecipientListType + oro_notification.form.handler.email_notification.class: Oro\Bundle\NotificationBundle\Form\Handler\EmailNotificationHandler + + # Email notification API + oro_notification.email_notification.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager + oro_notification.form.type.email_notification.api.class: Oro\Bundle\NotificationBundle\Form\Type\EmailNotificationApiType + oro_notification.form.handler.email_notification.api.class: Oro\Bundle\NotificationBundle\Form\Handler\EmailNotificationHandler + + # Email notification services + oro_notification.mailer.transport.spool_db.class: Oro\Bundle\NotificationBundle\Provider\Mailer\DbSpool + oro_notification.mailer.transport.spool_entity.class: Oro\Bundle\NotificationBundle\Entity\SpoolItem + +services: + # Email notification form services + oro_notification.form.email_notification: + class: Symfony\Component\Form\Form + factory_method: createNamed + factory_service: form.factory + arguments: ["emailnotification", "emailnotification", null] + + oro_notification.form.type.email_notification: + class: %oro_notification.form.type.email_notification.class% + arguments: + - %oro_config.entities% + - @oro_email.form.subscriber.emailtemplate + tags: + - { name: form.type, alias: emailnotification } + + oro_notification.form.type.recipient_list: + class: %oro_notification.form.type.recipient_list.class% + arguments: + - @doctrine.orm.entity_manager + tags: + - { name: form.type, alias: oro_notification_recipient_list } + + oro_notification.form.handler.email_notification: + class: %oro_notification.form.handler.email_notification.class% + scope: request + arguments: + - @oro_notification.form.email_notification + - @request + - @doctrine.orm.entity_manager + + # Email notification API + oro_notification.email_notification.manager.api: + class: %oro_notification.email_notification.manager.api.class% + arguments: + - %oro_notification.emailnotification.entity.class% + - @doctrine.orm.entity_manager + + oro_notification.form.type.email_notification.api: + class: %oro_notification.form.type.email_notification.api.class% + arguments: + - %oro_config.entities% + - @oro_email.form.subscriber.emailtemplate + tags: + - { name: form.type, alias: emailnotification_api } + + oro_notification.form.email_notification.api: + class: Symfony\Component\Form\Form + factory_method: createNamed + factory_service: form.factory + arguments: ["emailnotification_api", "emailnotification_api", null] + + oro_notification.form.handler.email_notification.api: + class: %oro_notification.form.handler.email_notification.api.class% + scope: request + arguments: + - @oro_notification.form.email_notification.api + - @request + - @doctrine.orm.entity_manager + + # Event listeners + oro_notification.docrine.event.listener: + class: %oro_notification.doctrine_listener.class% + calls: + - [ setEventDispatcher, [ @event_dispatcher ] ] + tags: + - { name: doctrine.event_listener, event: postPersist } + - { name: doctrine.event_listener, event: postUpdate } + - { name: doctrine.event_listener, event: postRemove } + + # notification services + oro_notification.manager: + class: %oro_notification.manager.class% + arguments: + - @doctrine.orm.entity_manager + - %oro_notification.emailnotification.entity.class% + + oro_notification.email_handler: + class: %oro_notification.email_handler.class% + arguments: + - @oro_notification.twig + - @oro_notification.mailer + - @doctrine.orm.entity_manager + - %oro_notification.send_from% + - @logger + - @security.context + calls: + - [ setEnv, ['prod'] ] + - [ setMessageLimit, [100] ] + tags: + - { name: notification.handler, alias: email_notification_handler } + + # email notification Swift mailer with DB spool configured + oro_notification.mailer.spool_db: + class: %oro_notification.mailer.transport.spool_db.class% + arguments: + - @doctrine.orm.entity_manager + - %oro_notification.mailer.transport.spool_entity.class% + + swiftmailer.spool.db: + alias: oro_notification.mailer.spool_db + + oro_notification.mailer.transport: + class: Swift_Transport_SpoolTransport + arguments: + - @swiftmailer.transport.eventdispatcher + - @oro_notification.mailer.spool_db + + # notification mailer instance + oro_notification.mailer: + class: %swiftmailer.class% + arguments: + - @oro_notification.mailer.transport + + # notification twig instance + oro_notification.twig.string_loader: + class: Twig_Loader_String + + oro_notification.twig: + class: Twig_Environment + arguments: + - @oro_notification.twig.string_loader + calls: + # add extension sandbox using method call and not twig.extension tag cause we need to add sandbox only to this instance + - [ addExtension, [@oro_notification.twig.email_sandbox] ] + + oro_notification.twig.email_security_policy: + class: Twig_Sandbox_SecurityPolicy + arguments: + # tags + - [ 'if', 'app' ] + # filters + - [ 'upper', 'escape' ] + # methods + - + Oro\Bundle\UserBundle\Entity\User: [getusername, getfirstname, getlastname] + Oro\Bundle\TagBundle\Entity\Tag: [getcreatedby, getname] + # properties + - [] + # functions + - [] + + oro_notification.twig.email_sandbox: + class: Twig_Extension_Sandbox + arguments: + - @oro_notification.twig.email_security_policy + - true # use sandbox globally in instance diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/validation.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/validation.yml new file mode 100644 index 00000000000..659c7d5f54c --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/validation.yml @@ -0,0 +1,16 @@ +Oro\Bundle\NotificationBundle\Entity\EmailNotification: + properties: + event: + - NotBlank: ~ + template: + - NotBlank: ~ + entityName: + - NotBlank: ~ + +Oro\Bundle\NotificationBundle\Entity\RecipientList: + properties: + email: + - Email: ~ + constraints: + - Callback: + methods: [isValid] diff --git a/src/Oro/Bundle/NotificationBundle/Resources/translations/datagrid.en.yml b/src/Oro/Bundle/NotificationBundle/Resources/translations/datagrid.en.yml new file mode 100644 index 00000000000..1c91aa357a0 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/translations/datagrid.en.yml @@ -0,0 +1,15 @@ +oro: + notification: + datagrid: + action: + update: "Update" + delete: "Delete" + template: "Template" + event_name: "Event name" + recipients: "Recipients" + entity_name: "Entity name" + recipient: + user: "Recipient users" + group: "Recipient groups" + custom_email: "Recipient email" + owner: "Recipient entity owner" diff --git a/src/Oro/Bundle/NotificationBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/NotificationBundle/Resources/translations/messages.en.yml new file mode 100644 index 00000000000..6d92c5f80c3 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/translations/messages.en.yml @@ -0,0 +1,9 @@ +oro: + notification: + controller: + emailnotification: + saved.message: "Email notification rule has been saved" + event: + entity_post_update: "Entity update" + entity_post_persist: "Entity create" + entity_post_remove: "Entity remove" diff --git a/src/Oro/Bundle/NotificationBundle/Resources/translations/validators.en.yml b/src/Oro/Bundle/NotificationBundle/Resources/translations/validators.en.yml new file mode 100644 index 00000000000..a5f5aa2eb4e --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/translations/validators.en.yml @@ -0,0 +1,5 @@ +oro: + notification: + validators: + recipient_list: + empty.message: At least on recipient must be specified. diff --git a/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/entityName.html.twig b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/entityName.html.twig new file mode 100644 index 00000000000..7e8d2ea6098 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/entityName.html.twig @@ -0,0 +1,2 @@ +{% set config = oro_config_entity(value) %} +{{ config.name|trans({}, 'config') }} diff --git a/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/recipientList.html.twig b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/recipientList.html.twig new file mode 100644 index 00000000000..96a598aea70 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/Datagrid/Property/recipientList.html.twig @@ -0,0 +1,6 @@ +{% set valueArray = value|split(',') %} +
        + {% for recipient in valueArray if recipient is not empty %} +
      • {{ recipient }}
      • + {% endfor %} +
      diff --git a/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/index.html.twig b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/index.html.twig new file mode 100644 index 00000000000..e44f4c058eb --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/index.html.twig @@ -0,0 +1,15 @@ +{% extends 'OroUIBundle:actions:index.html.twig' %} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% set gridId = 'email-notification-grid' %} +{% block content %} + {% set pageTitle = 'Transactional Emails' %} + {% if resource_granted('oro_notification_emailnotification_create') %} + {% set buttons = [ + UI.addButton( + {'path' : path('oro_notification_emailnotification_create'), 'title' : 'Create notification rule', 'label' : 'Create notification rule'} + ) + ] + %} + {% endif %} + {{ parent() }} +{% endblock %} diff --git a/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/update.html.twig b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/update.html.twig new file mode 100644 index 00000000000..a9d426156a6 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/views/EmailNotification/update.html.twig @@ -0,0 +1,88 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with [ + 'OroUIBundle:Form:fields.html.twig', + 'OroUserBundle:Form:fields.html.twig', + 'OroNotificationBundle:Form:fields.html.twig', + 'OroEmailBundle:Form:fields.html.twig' +]%} +{% set title = form.vars.value.id ? 'Edit notification rule #%id%'|trans({'%id%': form.vars.value.id}) : 'Add Notification Rule'|trans %} +{% if form.vars.value.id %} + {% oro_title_set({params : {"%id%": form.vars.value.id} }) %} +{% endif %} + +{% set formAction = form.vars.value.id + ? path('oro_notification_emailnotification_update', { id: form.vars.value.id }) + : path('oro_notification_emailnotification_create') +%} + +{% block navButtons %} + {% if form.vars.value.id and resource_granted('oro_notification_emailnotification_remove') %} + {{ + UI.deleteButton({ + 'dataUrl': path('oro_api_delete_emailnotication', {'id': form.vars.value.id}), + 'dataRedirect': path('oro_notification_emailnotification_index'), + 'aCss': 'no-hash remove-button', + 'id': 'btn-remove-emailnotification', + 'dataId': form.vars.value.id, + 'dataMessage': 'Are you sure you want to delete this notification rule?', + 'title': 'Delete notification rule', + 'label': 'Delete' + }) + }} + {{ UI.buttonSeparator() }} + {% endif %} + {{ UI.button({'path' : path('oro_notification_emailnotification_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% if form.vars.value.id %} + {% set breadcrumbs = { + 'entity': form.vars.value, + 'indexPath': path('oro_notification_emailnotification_index'), + 'indexLabel': 'Transactional Emails', + 'entityTitle': title + } + %} + {{ parent() }} + {% else %} + {% include 'OroUIBundle::page_title_block.html.twig' %} + {% endif %} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'emailnotificaton-edit' %} + + {% set dataBlocks = [{ + 'title': 'General', + 'class': 'active', + 'subblocks': [{ + 'title': '', + 'data': [ + form_row(form.entityName), + form_row(form.event), + form_row(form.template), + ] + }] + }, + { + 'title': 'Recipient List', + 'class': '', + 'subblocks': [{ + 'title': '', + 'data': [ + form_widget(form.children.recipientList), + ] + }] + }] + %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + } + %} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/NotificationBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/NotificationBundle/Resources/views/Form/fields.html.twig new file mode 100644 index 00000000000..3d3314cb147 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Resources/views/Form/fields.html.twig @@ -0,0 +1,27 @@ +{% block _emailnotification_entityName_row %} + + {{ form_row(form) }} +{% endblock %} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Command/SpoolSendCommandTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Command/SpoolSendCommandTest.php new file mode 100644 index 00000000000..915b4a9d7da --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Command/SpoolSendCommandTest.php @@ -0,0 +1,62 @@ +command = new SpoolSendCommand(); + + $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $this->command->setContainer($this->container); + } + + public function testConfiguration() + { + $this->assertNotEmpty($this->command->getDescription()); + $this->assertEquals('oro:spool:send', $this->command->getName()); + } + + /** + * Test execute + */ + public function testExecute() + { + $mailer = $this->getMockBuilder('\Swift_Mailer') + ->disableOriginalConstructor() + ->getMock(); + + $this->container->expects($this->at(0)) + ->method('get') + ->with('oro_notification.mailer') + ->will($this->returnValue($mailer)); + + $this->container->expects($this->once()) + ->method('set') + ->with('mailer', $mailer) + ->will($this->returnSelf()); + + $this->container->expects($this->at(2)) + ->method('get') + ->with('mailer') + ->will($this->returnValue($mailer)); + + $input = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + + $this->command->run($input, $output); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php new file mode 100644 index 00000000000..0fa4b1a53c4 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php @@ -0,0 +1,125 @@ +getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $dispatcher = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + + $repository = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Repository\EventRepository') + ->disableOriginalConstructor() + ->getMock(); + + $container->expects($this->once()) + ->method('hasDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue(true)); + $container->expects($this->once()) + ->method('hasDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getDefinition') + ->with('event_dispatcher') + ->will($this->returnValue($dispatcher)); + + $container->expects($this->once()) + ->method('get') + ->with('doctrine.orm.entity_manager') + ->will($this->returnValue($this->configureEntityManagerMock($repository))); + + $container->expects($this->once()) + ->method('getParameter') + ->with('oro_notification.event_entity.class') + ->will($this->returnValue(self::CLASS_NAME)); + + $repository->expects($this->once()) + ->method('getEventNames') + ->will($this->returnValue(array(array('name' => self::EVENT_NAME)))); + + $dispatcher->expects($this->once()) + ->method('addMethodCall') + ->with( + 'addListenerService', + array(self::EVENT_NAME, array('oro_notification.manager', 'process')) + ); + + $compiler = new EventsCompilerPass(); + $compiler->process($container); + } + + public function testCompileManagerNotDefined() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + + $container->expects($this->once()) + ->method('hasDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue(false)); + + $container->expects($this->never()) + ->method('getDefinition'); + + $compiler = new EventsCompilerPass(); + $compiler->process($container); + } + + /** + * Creates and configure EM mock object + * + * @param $repository + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function configureEntityManagerMock($repository) + { + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $schemaManager = $this->getMockBuilder('Doctrine\DBAL\Schema\MySqlSchemaManager') + ->disableOriginalConstructor() + ->getMock(); + $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + + $schemaManager->expects($this->once()) + ->method('listTableNames') + ->will($this->returnValue(array('event_table_exist'))); + $connection->expects($this->once()) + ->method('getSchemaManager') + ->will($this->returnValue($schemaManager)); + + $metadata->expects($this->once()) + ->method('getTableName') + ->will($this->returnValue('event_table_exist')); + + $em->expects($this->once()) + ->method('getClassMetadata') + ->with(self::CLASS_NAME) + ->will($this->returnValue($metadata)); + + $em->expects($this->once()) + ->method('getRepository') + ->with('Oro\Bundle\NotificationBundle\Entity\Event') + ->will($this->returnValue($repository)); + + $em->expects($this->once()) + ->method('getConnection') + ->will($this->returnValue($connection)); + + return $em; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/NotificationHandlerPassTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/NotificationHandlerPassTest.php new file mode 100644 index 00000000000..b997c24d157 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/NotificationHandlerPassTest.php @@ -0,0 +1,72 @@ +compiler = new NotificationHandlerPass(); + } + + public function tearDown() + { + unset($this->compiler); + } + + public function testCompile() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + + $container->expects($this->once()) + ->method('hasDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue($definition)); + + $container->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('notification.handler') + ->will($this->returnValue(array(self::TEST_SERVICE_ID => null))); + + $definition->expects($this->once()) + ->method('addMethodCall') + ->with( + 'addHandler', + array(new Reference(self::TEST_SERVICE_ID)) + ); + + $this->compiler->process($container); + } + + public function testCompileManagerNotDefined() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + + $container->expects($this->once()) + ->method('hasDefinition') + ->with('oro_notification.manager') + ->will($this->returnValue(false)); + + $container->expects($this->never()) + ->method('getDefinition'); + + $this->compiler->process($container); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 00000000000..b9b65cb9b87 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,15 @@ +getConfigTreeBuilder(); + $this->assertInstanceOf('Symfony\Component\Config\Definition\Builder\TreeBuilder', $builder); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/OroNotificationExtensionTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/OroNotificationExtensionTest.php new file mode 100644 index 00000000000..86de9bd4daf --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/OroNotificationExtensionTest.php @@ -0,0 +1,19 @@ +getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $container->expects($this->exactly(2)) + ->method('addResource'); + + $extension->load($configs, $container); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EmailNotificationTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EmailNotificationTest.php new file mode 100644 index 00000000000..42924f01983 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EmailNotificationTest.php @@ -0,0 +1,112 @@ +entity = new EmailNotification(); + + // get id should return null cause this entity was not loaded from DB + $this->assertNull($this->entity->getId()); + } + + public function tearDown() + { + unset($this->entity); + } + + public function testGetterSetterForEntityName() + { + $this->assertNull($this->entity->getEntityName()); + $this->entity->setEntityName('testName'); + $this->assertEquals('testName', $this->entity->getEntityName()); + } + + public function testGetterSetterForTemplate() + { + $emailTemplate = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplate'); + $this->assertNull($this->entity->getTemplate()); + $this->entity->setTemplate($emailTemplate); + $this->assertEquals($emailTemplate, $this->entity->getTemplate()); + } + + public function testGetterSetterForEvent() + { + $this->assertNull($this->entity->getEvent()); + + $event = $this->getMock('Oro\Bundle\NotificationBundle\Entity\Event', array(), array('test.name')); + $this->entity->setEvent($event); + $this->assertEquals($event, $this->entity->getEvent()); + } + + public function testGetterSetterForRecipients() + { + $this->assertNull($this->entity->getRecipientList()); + + $list = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $this->entity->setRecipientList($list); + $this->assertEquals($list, $this->entity->getRecipientList()); + } + + public function testGetUsersRecipientsList() + { + $this->assertEmpty($this->entity->getRecipientUsersList()); + + $userMock1 = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $userMock1->expects($this->once())->method('getFullname') + ->will($this->returnValue('Test Name')); + $userMock1->expects($this->once())->method('getEmail') + ->will($this->returnValue('test@email1.com')); + + $userMock2 = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $userMock2->expects($this->once())->method('getFullname') + ->will($this->returnValue('Test2 Name2')); + $userMock2->expects($this->once())->method('getEmail') + ->will($this->returnValue('test@email2.com')); + + $collection = new ArrayCollection(array($userMock1, $userMock2)); + + $list = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $list->expects($this->once())->method('getUsers')->will($this->returnValue($collection)); + $this->entity->setRecipientList($list); + + $this->assertEquals( + 'Test Name , Test2 Name2 ', + $this->entity->getRecipientUsersList() + ); + } + + public function testGetGroupsRecipientsList() + { + $this->assertEmpty($this->entity->getRecipientGroupsList()); + + $groupMock1 = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $groupMock1->expects($this->once())->method('getName') + ->will($this->returnValue('Test Name')); + + $groupMock2 = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $groupMock2->expects($this->once())->method('getName') + ->will($this->returnValue('Test2 Name2')); + + $collection = new ArrayCollection(array($groupMock1, $groupMock2)); + + $list = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $list->expects($this->once())->method('getGroups')->will($this->returnValue($collection)); + $this->entity->setRecipientList($list); + + $this->assertEquals( + 'Test Name, Test2 Name2', + $this->entity->getRecipientGroupsList() + ); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EventTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EventTest.php new file mode 100644 index 00000000000..6ebbdff91dc --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/EventTest.php @@ -0,0 +1,45 @@ +event = new Event('test.name.from.construct'); + + // get id should return null cause this entity was not loaded from DB + $this->assertNull($this->event->getId()); + } + + public function tearDown() + { + unset($this->event); + } + + public function testSetterGetterForName() + { + $this->assertEquals('test.name.from.construct', $this->event->getName()); + $this->event->setName('test.new.name'); + $this->assertEquals('test.new.name', $this->event->getName()); + } + + public function testSetterGetterForDescription() + { + // empty from construct + $this->assertNull($this->event->getDescription()); + $this->event->setDescription('description'); + $this->assertEquals('description', $this->event->getDescription()); + + // set description from construct + $newInstance = new Event('test.name', 'test.description'); + $this->assertEquals('test.description', $newInstance->getDescription()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/RecipientListTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/RecipientListTest.php new file mode 100644 index 00000000000..2bc5286da63 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/RecipientListTest.php @@ -0,0 +1,178 @@ +entity = new RecipientList(); + + // get id should return null cause this entity was not loaded from DB + $this->assertNull($this->entity->getId()); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $this->entity->getUsers()); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $this->entity->getGroups()); + } + + public function tearDown() + { + unset($this->entity); + } + + public function testSetterGetterForUsers() + { + // test adding through array collection interface + $user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $this->entity->getUsers()->add($user); + + $this->assertContains($user, $this->entity->getUsers()); + + // clear collection + $this->entity->getUsers()->clear(); + $this->assertTrue($this->entity->getUsers()->isEmpty()); + + // test setter + $this->entity->addUser($user); + $this->assertContains($user, $this->entity->getUsers()); + + + // remove group + $this->entity->removeUser($user); + $this->assertTrue($this->entity->getUsers()->isEmpty()); + } + + public function testSetterGetterForGroups() + { + // test adding through array collection interface + $group = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $this->entity->getGroups()->add($group); + + $this->assertContains($group, $this->entity->getGroups()); + + // clear collection + $this->entity->getGroups()->clear(); + $this->assertTrue($this->entity->getGroups()->isEmpty()); + + // test setter + $this->entity->addGroup($group); + $this->assertContains($group, $this->entity->getGroups()); + + + // remove group + $this->entity->removeGroup($group); + $this->assertTrue($this->entity->getGroups()->isEmpty()); + } + + public function testSetterGetterForEmail() + { + $this->assertNull($this->entity->getEmail()); + + $this->entity->setEmail('test'); + $this->assertEquals('test', $this->entity->getEmail()); + } + + public function testSetterGetterForOwner() + { + $this->assertNull($this->entity->getOwner()); + + $this->entity->setOwner(true); + $this->assertEquals(true, $this->entity->getOwner()); + } + + public function testToString() + { + $group = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + // test when owner filled + $this->entity->setOwner(true); + $this->assertInternalType('string', $this->entity->__toString()); + $this->assertNotEmpty($this->entity->__toString()); + // clear owner + $this->entity->setOwner(null); + + // test when email filled + $this->entity->setEmail('test email'); + $this->assertInternalType('string', $this->entity->__toString()); + $this->assertNotEmpty($this->entity->__toString()); + // clear email + $this->entity->setEmail(null); + + // test when users filled + $this->entity->addUser($user); + $this->assertInternalType('string', $this->entity->__toString()); + $this->assertNotEmpty($this->entity->__toString()); + // clear users + $this->entity->getUsers()->clear(); + + // test when groups filled + $this->entity->addGroup($group); + $this->assertInternalType('string', $this->entity->__toString()); + $this->assertNotEmpty($this->entity->__toString()); + // clear groups + $this->entity->getGroups()->clear(); + + // should be empty if nothing filled + $this->assertEmpty($this->entity->__toString()); + } + + public function testNotValidData() + { + $context = $this->getMockBuilder('Symfony\Component\Validator\ExecutionContext') + ->disableOriginalConstructor() + ->getMock(); + + $context->expects($this->once()) + ->method('getPropertyPath') + ->will($this->returnValue('testPath')); + $context->expects($this->once()) + ->method('addViolationAt'); + + $this->entity->isValid($context); + } + + public function testValidData() + { + $group = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + $context = $this->getMockBuilder('Symfony\Component\Validator\ExecutionContext') + ->disableOriginalConstructor() + ->getMock(); + + $context->expects($this->never()) + ->method('getPropertyPath'); + $context->expects($this->never()) + ->method('addViolationAt'); + + //only users + $this->entity->addUser($user); + $this->entity->isValid($context); + // clear users + $this->entity->getUsers()->clear(); + + //only groups + $this->entity->addGroup($group); + $this->entity->isValid($context); + // clear groups + $this->entity->getGroups()->clear(); + + // only email + $this->entity->setEmail('test Email'); + $this->entity->isValid($context); + $this->entity->setEmail(null); + + // only owner + $this->entity->setOwner(true); + $this->entity->isValid($context); + $this->entity->setEmail(null); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EmailNotificationRepositoryTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EmailNotificationRepositoryTest.php new file mode 100644 index 00000000000..074977343f0 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EmailNotificationRepositoryTest.php @@ -0,0 +1,85 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->setMethods(array('createQueryBuilder')) + ->getMock(); + + $this->repository = new EmailNotificationRepository($this->entityManager, new ClassMetadata(self::ENTITY_NAME)); + $this->entity = new EmailNotification(); + $this->testEntities[] = $this->entity; + } + + protected function tearDown() + { + unset($this->repository); + unset($this->entityManager); + unset($this->testEntities); + unset($this->entity); + } + + public function testGetRules() + { + $query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery') + ->disableOriginalConstructor() + ->setMethods(array('getResult')) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('getResult') + ->will($this->returnValue($this->testEntities)); + + $entityAlias = 'emn'; + + $queryBuilder = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->setMethods(array('select', 'from', 'getQuery', 'leftJoin')) + ->getMock(); + $queryBuilder->expects($this->exactly(2))->method('select') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('from')->with(self::ENTITY_NAME, $entityAlias) + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('leftJoin')->with($entityAlias . '.event', 'event') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('getQuery') + ->will($this->returnValue($query)); + + $this->entityManager->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($queryBuilder)); + + $actualResult = $this->repository->getRules(); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $actualResult); + $this->assertCount(1, $actualResult); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EventRepositoryTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EventRepositoryTest.php new file mode 100644 index 00000000000..bbe6edd6b02 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/EventRepositoryTest.php @@ -0,0 +1,80 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->setMethods(array('createQueryBuilder')) + ->getMock(); + + $this->repository = new EventRepository($this->entityManager, new ClassMetadata(self::ENTITY_NAME)); + $this->event = new Event('test.event.name'); + $this->testEntities[] = $this->event; + } + + protected function tearDown() + { + unset($this->repository); + unset($this->entityManager); + unset($this->testEntities); + unset($this->event); + } + + public function testGetEventNames() + { + $query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery') + ->disableOriginalConstructor() + ->setMethods(array('getResult')) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('getResult') + ->will($this->returnValue($this->testEntities)); + + $entityAlias = 'e'; + + $queryBuilder = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->setMethods(array('select', 'from', 'getQuery')) + ->getMock(); + $queryBuilder->expects($this->exactly(2))->method('select') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('from')->with(self::ENTITY_NAME, $entityAlias) + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('getQuery') + ->will($this->returnValue($query)); + + $this->entityManager->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($queryBuilder)); + + $actualResult = $this->repository->getEventNames(); + $this->assertEquals($this->testEntities, $actualResult); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/RecipientListRepositoryTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/RecipientListRepositoryTest.php new file mode 100644 index 00000000000..ddb4f26334f --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/Repository/RecipientListRepositoryTest.php @@ -0,0 +1,115 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->setMethods(array('createQueryBuilder')) + ->getMock(); + + $this->repository = new RecipientListRepository($this->entityManager, new ClassMetadata(self::ENTITY_NAME)); + } + + protected function tearDown() + { + unset($this->repository); + unset($this->entityManager); + } + + public function testGetRecipientEmails() + { + $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $userMock->expects($this->once()) + ->method('getEmail') + ->will($this->returnValue('a@a.com')); + + $groupMock = $this->getMock('Oro\Bundle\UserBundle\Entity\Group'); + $groupMock->expects($this->once()) + ->method('getId') + ->will($this->returnValue(1)); + + $users = new ArrayCollection(array($userMock)); + $groups = new ArrayCollection(array($groupMock)); + + $recipientList = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $recipientList->expects($this->once()) + ->method('getUsers') + ->will($this->returnValue($users)); + $recipientList->expects($this->once()) + ->method('getGroups') + ->will($this->returnValue($groups)); + + $recipientList->expects($this->once()) + ->method('getOwner') + ->will($this->returnValue(true)); + + $recipientList->expects($this->exactly(2)) + ->method('getEmail') + ->will($this->returnValue('a@a.com')); + + $user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $user->expects($this->once()) + ->method('getEmail') + ->will($this->returnValue('b@b.com')); + + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + $entity->expects($this->once()) + ->method('getCreatedBy') + ->will($this->returnValue($user)); + + $query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery') + ->disableOriginalConstructor() + ->setMethods(array('getResult')) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('getResult') + ->will($this->returnValue(array(array('email' => 'b@b.com')))); + + $entityAlias = 'u'; + + $queryBuilder = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->setMethods(array('select', 'from', 'getQuery', 'leftJoin', 'where', 'setParameter')) + ->getMock(); + $queryBuilder->expects($this->once())->method('select') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('from')->with(self::ENTITY_NAME, $entityAlias) + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('getQuery') + ->will($this->returnValue($query)); + $queryBuilder->expects($this->once())->method('leftJoin')->with('u.groups', 'groups') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('where') + ->will($this->returnSelf()); + $queryBuilder->expects($this->once())->method('setParameter') + ->will($this->returnSelf()); + + $this->entityManager->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($queryBuilder)); + + + $emails = $this->repository->getRecipientEmails($recipientList, $entity); + $this->assertCount(2, $emails); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/SpoolItemTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/SpoolItemTest.php new file mode 100644 index 00000000000..2fd71843dec --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Entity/SpoolItemTest.php @@ -0,0 +1,42 @@ +entity = new SpoolItem(); + + // get id should return null cause this entity was not loaded from DB + $this->assertNull($this->entity->getId()); + } + + public function tearDown() + { + unset($this->entity); + } + + public function testSetterGetterStatus() + { + // empty from construct + $this->assertNull($this->entity->getStatus()); + $this->entity->setStatus('test.new.status'); + $this->assertEquals('test.new.status', $this->entity->getStatus()); + } + + public function testSetterGetterForMessage() + { + // empty from construct + $this->assertNull($this->entity->getMessage()); + $this->entity->setMessage('message'); + $this->assertEquals('message', $this->entity->getMessage()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/Handler/EmailNotificationHandlerTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/Handler/EmailNotificationHandlerTest.php new file mode 100644 index 00000000000..176521a8328 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/Handler/EmailNotificationHandlerTest.php @@ -0,0 +1,244 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->twig = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + $this->mailer = $this->getMockBuilder('\Swift_Mailer') + ->disableOriginalConstructor() + ->getMock(); + + $this->logger = $this->getMockBuilder('Monolog\Logger') + ->disableOriginalConstructor() + ->getMock(); + + $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + + $this->handler = new EmailNotificationHandler( + $this->twig, + $this->mailer, + $this->entityManager, + 'a@a.com', + $this->logger, + $this->securityContext + ); + $this->handler->setEnv('prod'); + $this->handler->setMessageLimit(10); + } + + protected function tearDown() + { + unset($this->entityManager); + unset($this->twig); + unset($this->mailer); + unset($this->handler); + } + + /** + * Test handler + */ + public function testHandle() + { + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + $event = $this->getMock('Oro\Bundle\NotificationBundle\Event\NotificationEvent', array(), array($entity)); + $event->expects($this->once()) + ->method('getEntity') + ->will($this->returnValue($entity)); + + $templateContent = "@subject = Test Subj\n@entityName = TestEntity"; + + $template = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplate'); + $template->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($templateContent)); + $template->expects($this->exactly(2)) + ->method('getType') + ->will($this->returnValue('html')); + $template->expects($this->once()) + ->method('getSubject') + ->will($this->returnValue('Test Subj')); + + $notification = $this->getMock('Oro\Bundle\NotificationBundle\Entity\EmailNotification'); + $notification->expects($this->once()) + ->method('getTemplate') + ->will($this->returnValue($template)); + + $recipientList = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $notification->expects($this->once()) + ->method('getRecipientList') + ->will($this->returnValue($recipientList)); + + $notifications = array( + $notification, + ); + + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + + $repo = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Repository\RecipientListRepository') + ->disableOriginalConstructor() + ->getMock(); + $repo->expects($this->once()) + ->method('getRecipientEmails') + ->with($recipientList, $entity) + ->will($this->returnValue(array('a@aa.com'))); + + $this->entityManager + ->expects($this->once()) + ->method('getRepository') + ->with('Oro\Bundle\NotificationBundle\Entity\RecipientList') + ->will($this->returnValue($repo)); + + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->isInstanceOf('\Swift_Message')); + + $this->addJob(); + + $this->handler->handle($event, $notifications); + } + + /** + * Test handler with expection and empty recipients + */ + public function testHandleErrors() + { + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + $event = $this->getMock('Oro\Bundle\NotificationBundle\Event\NotificationEvent', array(), array($entity)); + $event->expects($this->once()) + ->method('getEntity') + ->will($this->returnValue($entity)); + + $templateContent = "@subject = Test Subj\n@entityName = TestEntity"; + $template = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplate'); + $template->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($templateContent)); + $template->expects($this->once()) + ->method('getType') + ->will($this->returnValue('html')); + + $notification = $this->getMock('Oro\Bundle\NotificationBundle\Entity\EmailNotification'); + $notification->expects($this->once()) + ->method('getTemplate') + ->will($this->returnValue($template)); + + $recipientList = $this->getMock('Oro\Bundle\NotificationBundle\Entity\RecipientList'); + $notification->expects($this->once()) + ->method('getRecipientList') + ->will($this->returnValue($recipientList)); + + $notifications = array( + $notification, + ); + + $this->twig->expects($this->once()) + ->method('render') + ->will($this->throwException(new \Twig_Error('bla bla bla'))); + + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + + $repo = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Repository\RecipientListRepository') + ->disableOriginalConstructor() + ->getMock(); + $repo->expects($this->once()) + ->method('getRecipientEmails') + ->with($recipientList, $entity) + ->will($this->returnValue(array())); + + $this->entityManager + ->expects($this->once()) + ->method('getRepository') + ->with('Oro\Bundle\NotificationBundle\Entity\RecipientList') + ->will($this->returnValue($repo)); + + $this->handler->handle($event, $notifications); + } + + public function testNotify() + { + $params = $this->getMock('Symfony\Component\HttpFoundation\ParameterBag'); + $params->expects($this->once()) + ->method('get') + ->with('to') + ->will($this->returnValue(array())); + + $this->assertFalse($this->handler->notify($params)); + } + + /** + * add job assertions + */ + public function addJob() + { + $query = $this->getMock( + 'Doctrine\ORM\AbstractQuery', + array('getSQL', 'setMaxResults', 'getOneOrNullResult', 'setParameter', '_doExecute'), + array(), + '', + false + ); + + $query->expects($this->once())->method('getOneOrNullResult') + ->will($this->returnValue(null)); + $query->expects($this->exactly(2))->method('setParameter') + ->will($this->returnSelf()); + $query->expects($this->once()) + ->method('setMaxResults') + ->with($this->equalTo(1)) + ->will($this->returnSelf()); + + $this->entityManager->expects($this->once()) + ->method('createQuery') + ->will($this->returnValue($query)); + + $this->entityManager->expects($this->once()) + ->method('persist'); + $this->entityManager->expects($this->once()) + ->method('flush'); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/NotificationEventTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/NotificationEventTest.php new file mode 100644 index 00000000000..69f9ea01a8e --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Event/NotificationEventTest.php @@ -0,0 +1,31 @@ +entity = new \stdClass(); + $this->event = new NotificationEvent($this->entity); + } + + public function testGetEntity() + { + $this->assertEquals($this->entity, $this->event->getEntity()); + $this->event->setEntity(null); + $this->assertEquals(null, $this->event->getEntity()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Entity/FakeEntity.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Entity/FakeEntity.php new file mode 100644 index 00000000000..047ff6cf7b1 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Entity/FakeEntity.php @@ -0,0 +1,22 @@ +getSomething()->dispatch('oro.event.good_happens_unittest', $someObj); + } + + public function getSomething() + { + return $this->something; + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Resources/emails/test.template.html.twig b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Fixtures/Resources/emails/test.template.html.twig new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Handler/EmailNotificationHandlerTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Handler/EmailNotificationHandlerTest.php new file mode 100644 index 00000000000..c538c86f7ea --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Handler/EmailNotificationHandlerTest.php @@ -0,0 +1,118 @@ +form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + $this->request = new Request(); + $this->manager = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->entity = new EmailNotification(); + $this->handler = new EmailNotificationHandler($this->form, $this->request, $this->manager); + } + + public function testProcessUnsupportedRequest() + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->form->expects($this->never()) + ->method('submit'); + + $this->assertFalse($this->handler->process($this->entity)); + } + + /** + * @dataProvider supportedMethods + * @param string $method + */ + public function testProcessSupportedRequest($method) + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->request->setMethod($method); + + $this->form->expects($this->once()) + ->method('submit') + ->with($this->request); + + $this->assertFalse($this->handler->process($this->entity)); + } + + public function supportedMethods() + { + return array( + array('POST'), + array('PUT') + ); + } + + public function testProcessValidData() + { + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->request->setMethod('POST'); + + $this->form->expects($this->once()) + ->method('submit') + ->with($this->request); + + $this->form->expects($this->once()) + ->method('isValid') + ->will($this->returnValue(true)); + + $this->manager->expects($this->once()) + ->method('persist') + ->with($this->entity); + + $this->manager->expects($this->once()) + ->method('flush'); + + $this->assertTrue($this->handler->process($this->entity)); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationApiTypeTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationApiTypeTest.php new file mode 100644 index 00000000000..acf9c2813e9 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationApiTypeTest.php @@ -0,0 +1,54 @@ +getMockBuilder('Oro\Bundle\EmailBundle\Form\EventListener\BuildTemplateFormSubscriber') + ->disableOriginalConstructor() + ->getMock(); + + $this->type = new EmailNotificationApiType(array(), $listener); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('emailnotification_api', $this->type->getName()); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->at(0)) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Oro\Bundle\EmailBundle\Form\EventListener\BuildTemplateFormSubscriber')); + + $builder->expects($this->at(5)) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber')); + + $this->type->buildForm($builder, array()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationTypeTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationTypeTest.php new file mode 100644 index 00000000000..03e8bc0e6c4 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/EmailNotificationTypeTest.php @@ -0,0 +1,57 @@ +getMockBuilder('Oro\Bundle\EmailBundle\Form\EventListener\BuildTemplateFormSubscriber') + ->disableOriginalConstructor() + ->getMock(); + + $entitiesConfig = array( + 'Oro\Bundle\UserBundle\Entity\User' => array('name' => 'bla bla') + ); + + $this->type = new EmailNotificationType($entitiesConfig, $listener); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('emailnotification', $this->type->getName()); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(4)) + ->method('add'); + + $builder->expects($this->once()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Oro\Bundle\EmailBundle\Form\EventListener\BuildTemplateFormSubscriber')); + + $this->type->buildForm($builder, array()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/RecipientListTypeTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/RecipientListTypeTest.php new file mode 100644 index 00000000000..b3f2761e5ee --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Form/Type/RecipientListTypeTest.php @@ -0,0 +1,60 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $this->type = new RecipientListType($this->entityManager); + } + + public function tearDown() + { + unset($this->type); + unset($this->entityManager); + } + + public function testGetName() + { + $this->assertEquals('oro_notification_recipient_list', $this->type->getName()); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(4)) + ->method('add'); + + $this->type->buildForm($builder, array()); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/OroNotificationBundleTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/OroNotificationBundleTest.php new file mode 100644 index 00000000000..a5754e228e4 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/OroNotificationBundleTest.php @@ -0,0 +1,30 @@ +getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + + $container->expects($this->at(0)) + ->method('addCompilerPass') + ->with($this->isInstanceOf('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface')) + ->will($this->returnSelf()); + + $container->expects($this->at(1)) + ->method('addCompilerPass') + ->with( + $this->isInstanceOf('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface'), + $this->equalTo(PassConfig::TYPE_AFTER_REMOVING) + ) + ->will($this->returnSelf()); + + $bundle = new OroNotificationBundle($kernel); + $bundle->build($container); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DbSpoolTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DbSpoolTest.php new file mode 100644 index 00000000000..e3dd4aa4942 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DbSpoolTest.php @@ -0,0 +1,169 @@ +em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $this->className = 'Oro\Bundle\NotificationBundle\Entity\SpoolItem'; + + $this->spool = new DbSpool($this->em, $this->className); + + $this->spool->start(); + $this->spool->stop(); + $this->assertTrue($this->spool->isStarted()); + } + + /** + * Test adding to spool/queueing message + */ + public function testQueueMessage() + { + $message = $this->getMock('\Swift_Mime_Message'); + + $this->em + ->expects($this->once()) + ->method('persist') + ->with($this->isInstanceOf($this->className)); + + $this->em + ->expects($this->once()) + ->method('flush') + ->with($this->isInstanceOf($this->className)); + + $this->assertTrue($this->spool->queueMessage($message)); + } + + /** + * Test adding to spool/queueing message + * @expectedException \Swift_IoException + */ + public function testQueueMessageException() + { + $message = $this->getMock('\Swift_Mime_Message'); + + $this->em + ->expects($this->once()) + ->method('persist') + ->with($this->isInstanceOf($this->className)); + + $this->em + ->expects($this->once()) + ->method('flush') + ->will($this->throwException(new \Exception('problem'))); + + $this->spool->queueMessage($message); + } + + public function testFlushMessage() + { + $transport = $this->getMock('\Swift_Transport'); + + $transport->expects($this->once()) + ->method('isStarted') + ->will($this->returnValue(false)); + $transport->expects($this->once()) + ->method('start'); + + $message = $this->getMock('\Swift_Mime_Message'); + $messageSerialized = serialize($message); + + $spoolItem = $this->getMock($this->className); + $spoolItem->expects($this->once()) + ->method('setStatus'); + $spoolItem->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($messageSerialized)); + $emails = array( + $spoolItem, + ); + + $this->em + ->expects($this->once()) + ->method('persist') + ->with($this->isInstanceOf($this->className)); + $this->em + ->expects($this->exactly(2)) + ->method('flush'); + $this->em + ->expects($this->once()) + ->method('remove'); + + $repository = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Repository\SpoolItemRepository') + ->disableOriginalConstructor() + ->getMock(); + $repository->expects($this->once()) + ->method('findBy') + ->will($this->returnValue($emails)); + + $this->em + ->expects($this->once()) + ->method('getRepository') + ->with($this->className) + ->will($this->returnValue($repository)); + + $transport->expects($this->once()) + ->method('send') + ->with($message, array()) + ->will($this->returnValue(1)); + + $this->spool->setTimeLimit(-100); + $count = $this->spool->flushQueue($transport); + $this->assertEquals(1, $count); + } + + public function testFlushMessageZeroEmails() + { + $transport = $this->getMock('\Swift_Transport'); + + $transport->expects($this->once()) + ->method('isStarted') + ->will($this->returnValue(false)); + $transport->expects($this->once()) + ->method('start'); + + $repository = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Repository\SpoolItemRepository') + ->disableOriginalConstructor() + ->getMock(); + $repository->expects($this->once()) + ->method('findBy') + ->will($this->returnValue(array())); + + $this->em + ->expects($this->once()) + ->method('getRepository') + ->with($this->className) + ->will($this->returnValue($repository)); + + $count = $this->spool->flushQueue($transport); + $this->assertEquals(0, $count); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DoctrineListenerTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DoctrineListenerTest.php new file mode 100644 index 00000000000..09d72b0d744 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/DoctrineListenerTest.php @@ -0,0 +1,77 @@ +listener = new DoctrineListener(); + $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + + $this->listener->setEventDispatcher($this->eventDispatcher); + $this->assertEquals($this->eventDispatcher, $this->listener->getEventDispatcher()); + } + + public function tearDown() + { + unset($this->listener); + unset($this->eventDispatcher); + } + + /** + * @dataProvider eventData + * @param $methodName + * @param $eventName + */ + public function testEventDispatchers($methodName, $eventName) + { + $args = $this->getMockBuilder('Doctrine\ORM\Event\LifecycleEventArgs') + ->disableOriginalConstructor() + ->getMock(); + $args->expects($this->once()) + ->method('getEntity') + ->will($this->returnValue('something')); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo($eventName), $this->isInstanceOf('Symfony\Component\EventDispatcher\Event')); + + $this->listener->$methodName($args); + } + + /** + * data provider + */ + public function eventData() + { + return array( + 'post update event case' => array( + 'method name' => 'postUpdate', + 'expected event name' => 'oro.notification.event.entity_post_update' + ), + 'post persist event case' => array( + 'method name' => 'postPersist', + 'expected event name' => 'oro.notification.event.entity_post_persist' + ), + 'post remove event case' => array( + 'method name' => 'postRemove', + 'expected event name' => 'oro.notification.event.entity_post_remove' + ), + ); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/NotificationManagerTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/NotificationManagerTest.php new file mode 100644 index 00000000000..2d1f2cae375 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/Provider/NotificationManagerTest.php @@ -0,0 +1,143 @@ +em = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); + $this->className = 'Oro\Bundle\NotificationBundle\Entity\EmailNotification'; + $this->handler = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Event\Handler\EmailNotificationHandler') + ->disableOriginalConstructor() + ->getMock(); + $this->entity = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainAuthorInterface'); + $this->rules = new ArrayCollection(array()); + + $repository = $this->getMockBuilder( + 'Oro\Bundle\NotificationBundle\Entity\Repository\EmailNotificationRepository' + )->disableOriginalConstructor()->getMock(); + + $repository->expects($this->once())->method('getRules') + ->will($this->returnValue($this->rules)); + + $this->em->expects($this->once())->method('getRepository') + ->with($this->equalTo($this->className)) + ->will($this->returnValue($repository)); + + $this->manager = new NotificationManager($this->em, $this->className); + $this->manager->addHandler($this->handler); + } + + public function tearDown() + { + unset($this->em); + unset($this->className); + unset($this->handler); + unset($this->entity); + unset($this->rules); + unset($this->manager); + } + + /** + * @dataProvider dataProvider + */ + public function testProcess($eventPropagationStopped) + { + $notificationEventMock = $this->getMock( + 'Oro\Bundle\NotificationBundle\Event\NotificationEvent', + array(), + array($this->entity) + ); + $notificationEventMock->expects($this->once())->method('getEntity') + ->will($this->returnValue($this->entity)); + $notificationEventMock->expects($this->once())->method('getName') + ->will($this->returnValue(self::TEST_EVENT_NAME)); + $notificationEventMock->expects($this->once())->method('isPropagationStopped') + ->will($this->returnValue($eventPropagationStopped)); + + $event = $this->getMockBuilder('Oro\Bundle\NotificationBundle\Entity\Event') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->at(0))->method('getName') + ->will($this->returnValue(self::TEST_EVENT_NAME)); + $event->expects($this->at(1))->method('getName') + ->will($this->returnValue(self::TEST_EVENT_NAME . ' not the same')); + + $this->handler->expects($this->once())->method('handle'); + + $rule = $this->getMock($this->className); + $rule->expects($this->exactly(2))->method('getEntityName') + ->will($this->returnValue(get_class($this->entity))); + $rule->expects($this->exactly(2))->method('getEvent') + ->will($this->returnValue($event)); + + $this->rules->add($rule); + $this->rules->add($rule); + + $this->manager->process($notificationEventMock); + } + + /** + * @return array + */ + public function dataProvider() + { + return array( + array(false), + array(true), + ); + } + + /** + * Test setters, getters + */ + public function testAddAndGetHandlers() + { + $this->assertCount(1, $this->manager->getHandlers()); + + $handler = $this->getMock('Oro\Bundle\NotificationBundle\Event\Handler\EventHandlerInterface'); + $this->manager->addHandler($handler); + + $this->assertCount(2, $this->manager->getHandlers()); + $this->assertContains($handler, $this->manager->getHandlers()); + } +} diff --git a/src/Oro/Bundle/NotificationBundle/Tests/bootstrap.php b/src/Oro/Bundle/NotificationBundle/Tests/bootstrap.php new file mode 100644 index 00000000000..e77e3a01e50 --- /dev/null +++ b/src/Oro/Bundle/NotificationBundle/Tests/bootstrap.php @@ -0,0 +1,14 @@ +=5.3.3", + "symfony/symfony": "2.1.*" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\NotificationBundle": "" } + }, + "target-dir": "Oro/Bundle/NotificationBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/NotificationBundle/readme.md b/src/Oro/Bundle/NotificationBundle/readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Oro/Bundle/OrganizationBundle/Controller/Api/Rest/BusinessUnitController.php b/src/Oro/Bundle/OrganizationBundle/Controller/Api/Rest/BusinessUnitController.php new file mode 100644 index 00000000000..ed1689a3aae --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Controller/Api/Rest/BusinessUnitController.php @@ -0,0 +1,140 @@ +getRequest()->get('page', 1); + $limit = (int)$this->getRequest()->get('limit', self::ITEMS_PER_PAGE); + + return $this->handleGetListRequest($page, $limit); + } + + /** + * Create new business unit + * + * @ApiDoc( + * description="Create new business unit", + * resource=true + * ) + * @AclAncestor("oro_business_unit_create") + */ + public function postAction() + { + return $this->handleCreateRequest(); + } + + /** + * REST PUT + * + * @param int $id Business unit item id + * + * @ApiDoc( + * description="Update business unit", + * resource=true + * ) + * @AclAncestor("oro_business_unit_update") + * @return Response + */ + public function putAction($id) + { + return $this->handleUpdateRequest($id); + } + + /** + * REST GET item + * + * @param string $id + * + * @ApiDoc( + * description="Get business unit item", + * resource=true + * ) + * @AclAncestor("oro_business_unit_view") + * @return Response + */ + public function getAction($id) + { + return $this->handleGetRequest($id); + } + + /** + * REST DELETE + * + * @param int $id + * + * @ApiDoc( + * description="Delete business unit", + * resource=true + * ) + * @Acl( + * id="oro_business_unit_delete", + * name="Delete business units", + * description="User can delete business units", + * parent="oro_business_unit" + * ) + * @return Response + */ + public function deleteAction($id) + { + return $this->handleDeleteRequest($id); + } + + /** + * {@inheritdoc} + */ + public function getManager() + { + return $this->get('oro_organization.business_unit.manager.api'); + } + + /** + * {@inheritdoc} + */ + public function getForm() + { + return $this->get('oro_organization.form.business_unit.api'); + } + + /** + * {@inheritdoc} + */ + public function getFormHandler() + { + return $this->get('oro_organization.form.handler.api'); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Controller/Api/Soap/BusinessUnitController.php b/src/Oro/Bundle/OrganizationBundle/Controller/Api/Soap/BusinessUnitController.php new file mode 100644 index 00000000000..ba246a92328 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Controller/Api/Soap/BusinessUnitController.php @@ -0,0 +1,94 @@ +handleGetListRequest($page, $limit); + } + + /** + * @Soap\Method("getBusinessUnit") + * @Soap\Param("id", phpType = "int") + * @Soap\Result(phpType = "Oro\Bundle\OrganizationBundle\Entity\BusinessUnit") + * @AclAncestor("oro_business_unit_view") + */ + public function getAction($id) + { + return $this->handleGetRequest($id); + } + + /** + * @Soap\Method("createBusinessUnit") + * @Soap\Param("business_unit", phpType = "Oro\Bundle\OrganizationBundle\Entity\BusinessUnit") + * @Soap\Result(phpType = "boolean") + * @AclAncestor("oro_business_unit_create") + */ + public function createAction($business_unit) + { + return $this->handleCreateRequest(); + } + + /** + * @Soap\Method("updateBusinessUnit") + * @Soap\Param("id", phpType = "int") + * @Soap\Param("business_unit", phpType = "Oro\Bundle\OrganizationBundle\Entity\BusinessUnit") + * @Soap\Result(phpType = "boolean") + * @AclAncestor("oro_business_unit_update") + */ + public function updateAction($id, $business_unit) + { + return $this->handleUpdateRequest($id); + } + + /** + * @Soap\Method("deleteBusinessUnit") + * @Soap\Param("id", phpType = "int") + * @Soap\Result(phpType = "boolean") + * @AclAncestor("oro_business_unit_delete") + */ + public function deleteAction($id) + { + return $this->handleDeleteRequest($id); + } + + /** + * {@inheritdoc} + */ + public function getManager() + { + return $this->container->get('oro_organization.business_unit.manager.api'); + } + + /** + * {@inheritdoc} + */ + public function getForm() + { + return $this->container->get('oro_organization.form.business_unit.api'); + } + + /** + * {@inheritdoc} + */ + public function getFormHandler() + { + return $this->container->get('oro_organization.form.handler.api'); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Controller/BusinessUnitController.php b/src/Oro/Bundle/OrganizationBundle/Controller/BusinessUnitController.php new file mode 100644 index 00000000000..7a4c321e2b6 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Controller/BusinessUnitController.php @@ -0,0 +1,180 @@ +updateAction(new BusinessUnit()); + } + + /** + * @Route("/view/{id}", name="oro_business_unit_view", requirements={"id"="\d+"}) + * @Template + * @Acl( + * id="oro_business_unit_view", + * name="View business unit", + * description="View business unit", + * parent="oro_business_unit" + * ) + */ + public function viewAction(BusinessUnit $entity) + { + return array( + 'datagrid' => $this->getBusinessUnitDatagridManager($entity, 'view')->getDatagrid()->createView(), + 'entity' => $entity, + ); + } + + /** + * Edit business_unit form + * + * @Route("/update/{id}", name="oro_business_unit_update", requirements={"id"="\d+"}, defaults={"id"=0}) + * @Template + * @Acl( + * id="oro_business_unit_update", + * name="Edit business_unit", + * description="Edit business_unit", + * parent="oro_business_unit" + * ) + */ + public function updateAction(BusinessUnit $entity) + { + if ($this->get('oro_organization.form.handler.business_unit')->process($entity)) { + $this->get('session')->getFlashBag()->add('success', 'Business Unit successfully saved'); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_business_unit_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_business_unit_index', + ) + ); + } + + return array( + 'datagrid' => $this->getBusinessUnitDatagridManager($entity, 'update')->getDatagrid()->createView(), + 'form' => $this->get('oro_organization.form.business_unit')->createView(), + ); + } + + /** + * @Route( + * "/{_format}", + * name="oro_business_unit_index", + * requirements={"_format"="html|json"}, + * defaults={"_format" = "html"} + * ) + * @Acl( + * id="oro_business_unit_list", + * name="View business_unit list", + * description="List of business_units", + * parent="oro_business_unit" + * ) + * @Template() + */ + public function indexAction(Request $request) + { + /** @var BusinessUnitDatagridManager $gridManager */ + $gridManager = $this->get('oro_organization.business_unit_datagrid_manager'); + $datagridView = $gridManager->getDatagrid()->createView(); + + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } + + return array('datagrid' => $datagridView); + } + + /** + * Get grid users data + * + * @Route( + * "/update_grid/{id}", + * name="oro_business_update_unit_user_grid", + * requirements={"id"="\d+"}, + * defaults={"id"=0, "_format"="json"} + * ) + * @AclAncestor("oro_business_unit_list") + */ + public function updateGridDataAction(BusinessUnit $entity = null) + { + if (!$entity) { + $entity = new BusinessUnit(); + } + $datagridView = $this->getBusinessUnitDatagridManager($entity, 'update') + ->getDatagrid()->createView(); + + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } + + /** + * Get grid users data + * + * @Route( + * "/view_grid/{id}", + * name="oro_business_view_unit_user_grid", + * requirements={"id"="\d+"}, + * defaults={"_format"="json"} + * ) + * @AclAncestor("oro_business_unit_list") + */ + public function viewGridDataAction(BusinessUnit $entity) + { + $datagridView = $this->getBusinessUnitDatagridManager($entity, 'view') + ->getDatagrid()->createView(); + + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } + + /** + * @param BusinessUnit $businessUnit + * @param string $action + * @return BusinessUnitUpdateUserDatagridManager + */ + protected function getBusinessUnitDatagridManager(BusinessUnit $businessUnit, $action) + { + /** @var $result BusinessUnitUpdateUserDatagridManager */ + $result = $this->get('oro_organization.business_unit_' . $action . '_user_datagrid_manager'); + $result->setBusinessUnit($businessUnit); + $result->getRouteGenerator()->setRouteParameters(array('id' => $businessUnit->getId())); + + return $result; + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/DataFixtures/ORM/LoadOrganizationData.php b/src/Oro/Bundle/OrganizationBundle/DataFixtures/ORM/LoadOrganizationData.php new file mode 100644 index 00000000000..a446baae971 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/DataFixtures/ORM/LoadOrganizationData.php @@ -0,0 +1,23 @@ +setName('default') + ->setCurrency('USD') + ->setPrecision('000 000.00'); + + $manager->persist($defaultOrganization); + $manager->flush(); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitDatagridManager.php b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitDatagridManager.php new file mode 100644 index 00000000000..d3a4d905277 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitDatagridManager.php @@ -0,0 +1,231 @@ +router, 'oro_business_unit_view', array('id')), + new UrlProperty('update_link', $this->router, 'oro_business_unit_update', array('id')), + new UrlProperty('delete_link', $this->router, 'oro_api_delete_businessunit', array('id')), + ); + } + + /** + * {@inheritDoc} + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function configureFields(FieldDescriptionCollection $fieldsCollection) + { + $fieldId = new FieldDescription(); + $fieldId->setName('id'); + $fieldId->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => 'ID', + 'field_name' => 'id', + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'required' => false, + 'sortable' => false, + 'filterable' => false, + 'show_filter' => false, + 'show_column' => false, + ) + ); + $fieldsCollection->add($fieldId); + + $fieldName = new FieldDescription(); + $fieldName->setName('name'); + $fieldName->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Name', + 'field_name' => 'name', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldName); + + $fieldEmail = new FieldDescription(); + $fieldEmail->setName('email'); + $fieldEmail->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Email', + 'field_name' => 'email', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldEmail); + + $fieldPhone = new FieldDescription(); + $fieldPhone->setName('phone'); + $fieldPhone->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Phone', + 'field_name' => 'phone', + 'filter_type' => FilterInterface::TYPE_STRING, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldPhone); + + $parent = new FieldDescription(); + $parent->setName('parent'); + $parent->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Parent', + 'field_name' => 'parentName', + 'expression' => 'parent', + 'filter_type' => FilterInterface::TYPE_ENTITY, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + // entity filter options + 'class' => 'OroOrganizationBundle:BusinessUnit', + 'property' => 'name', + 'filter_by_where' => true, + ) + ); + $fieldsCollection->add($parent); + + $organization = new FieldDescription(); + $organization->setName('organization'); + $organization->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'label' => 'Organization', + 'field_name' => 'organizationName', + 'expression' => 'organization', + 'filter_type' => FilterInterface::TYPE_ENTITY, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + // entity filter options + 'class' => 'OroOrganizationBundle:Organization', + 'property' => 'name', + 'filter_by_where' => true, + ) + ); + $fieldsCollection->add($organization); + + $fieldCreated = new FieldDescription(); + $fieldCreated->setName('created_at'); + $fieldCreated->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_DATETIME, + 'label' => 'Created at', + 'field_name' => 'created_at', + 'filter_type' => FilterInterface::TYPE_DATETIME, + 'required' => false, + 'sortable' => true, + 'filterable' => true, + 'show_filter' => true, + ) + ); + $fieldsCollection->add($fieldCreated); + } + + /** + * {@inheritDoc} + */ + protected function prepareQuery(ProxyQueryInterface $query) + { + $entityAlias = $query->getRootAlias(); + $query->addSelect('organization.name as organizationName', true); + $query->addSelect('parent.name as parentName', true); + /** @var $query QueryBuilder */ + $query->leftJoin($entityAlias . '.organization', 'organization'); + $query->leftJoin($entityAlias . '.parent', 'parent'); + } + + /** + * {@inheritDoc} + */ + protected function getRowActions() + { + $businessUnitClickAction = array( + 'name' => 'rowClick', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_business_unit_view', + 'options' => array( + 'label' => 'View', + 'link' => 'view_link', + 'runOnRowClick' => true, + ) + ); + + $businessUnitViewAction = array( + 'name' => 'view', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_business_unit_view', + 'options' => array( + 'label' => 'View', + 'icon' => 'user', + 'link' => 'view_link', + ) + ); + + $businessUnitUpdateAction = array( + 'name' => 'edit', + 'type' => ActionInterface::TYPE_REDIRECT, + 'acl_resource' => 'oro_business_unit_update', + 'options' => array( + 'label' => 'Update', + 'icon' => 'edit', + 'link' => 'update_link', + ) + ); + + $businessUnitDeleteAction = array( + 'name' => 'delete', + 'type' => ActionInterface::TYPE_DELETE, + 'acl_resource' => 'oro_business_unit_delete', + 'options' => array( + 'label' => 'Delete', + 'icon' => 'trash', + 'link' => 'delete_link', + ) + ); + + return array( + $businessUnitClickAction, + $businessUnitViewAction, + $businessUnitUpdateAction, + $businessUnitDeleteAction + ); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitUpdateUserDatagridManager.php b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitUpdateUserDatagridManager.php new file mode 100644 index 00000000000..b283f92bbc2 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitUpdateUserDatagridManager.php @@ -0,0 +1,135 @@ +businessUnit = $businessUnit; + } + + /** + * @return BusinessUnit + * @throws \LogicException When business unit is not set + */ + public function getBusinessUnit() + { + if (!$this->businessUnit) { + throw new \LogicException('Datagrid manager has no configured BusinessUnit entity'); + } + + return $this->businessUnit; + } + + /** + * {@inheritDoc} + */ + protected function createUserRelationColumn() + { + $fieldHasBusinessUnit = new FieldDescription(); + $fieldHasBusinessUnit->setName('has_business_unit'); + $fieldHasBusinessUnit->setOptions( + array( + 'type' => FieldDescriptionInterface::TYPE_BOOLEAN, + 'label' => 'Has business unit', + 'field_name' => 'hasCurrentBusinessUnit', + 'expression' => $this->getHasBusinessUnitExpression(), + 'nullable' => false, + 'editable' => true, + 'sortable' => true, + 'filter_type' => FilterInterface::TYPE_BOOLEAN, + 'filterable' => true, + 'show_filter' => true, + 'filter_by_where' => true + ) + ); + + return $fieldHasBusinessUnit; + } + + /** + * {@inheritDoc} + */ + protected function prepareQuery(ProxyQueryInterface $query) + { + $query->addSelect($this->getHasBusinessUnitExpression() . ' AS hasCurrentBusinessUnit', true); + + return $query; + } + + /** + * @return string + */ + protected function getHasBusinessUnitExpression() + { + if (null === $this->hasBusinessUnitExpression) { + /** @var EntityQueryFactory $queryFactory */ + $queryFactory = $this->queryFactory; + $entityAlias = $queryFactory->getAlias(); + + if ($this->getBusinessUnit()->getId()) { + $this->hasBusinessUnitExpression = + "CASE WHEN " . + "(:business_unit MEMBER OF $entityAlias.businessUnits OR $entityAlias.id IN (:data_in)) AND " . + "$entityAlias.id NOT IN (:data_not_in) ". + "THEN true ELSE false END"; + } else { + $this->hasBusinessUnitExpression = + "CASE WHEN " . + "$entityAlias.id IN (:data_in) AND $entityAlias.id NOT IN (:data_not_in) " . + "THEN true ELSE false END"; + } + } + + return $this->hasBusinessUnitExpression; + } + + /** + * {@inheritDoc} + */ + protected function getQueryParameters() + { + $parameters = parent::getQueryParameters(); + + if ($this->getBusinessUnit()->getId()) { + $parameters['business_unit'] = $this->getBusinessUnit(); + } + + return $parameters; + } + + /** + * @return array + */ + protected function getDefaultSorters() + { + return array( + 'has_business_unit' => SorterInterface::DIRECTION_DESC, + 'lastName' => SorterInterface::DIRECTION_ASC, + ); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitViewUserDatagridManager.php b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitViewUserDatagridManager.php new file mode 100644 index 00000000000..bbe407f3b56 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Datagrid/BusinessUnitViewUserDatagridManager.php @@ -0,0 +1,68 @@ +getRootAlias(); + $query->andWhere(":business_unit MEMBER OF $entityAlias.businessUnits"); + return $query; + } + + /** + * {@inheritDoc} + */ + protected function getQueryParameters() + { + return array('business_unit' => $this->getBusinessUnit()); + } + + /** + * @return array + */ + protected function getDefaultSorters() + { + return array( + 'lastName' => SorterInterface::DIRECTION_ASC, + ); + } + + /** + * {@inheritDoc} + */ + protected function getProperties() + { + return array(); + } + + /** + * {@inheritdoc} + */ + protected function getRowActions() + { + return array(); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/OrganizationBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..7e232278c11 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/DependencyInjection/Configuration.php @@ -0,0 +1,20 @@ +root('oro_organization'); + + return $treeBuilder; + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/DependencyInjection/OroOrganizationExtension.php b/src/Oro/Bundle/OrganizationBundle/DependencyInjection/OroOrganizationExtension.php new file mode 100644 index 00000000000..18961d50bdd --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/DependencyInjection/OroOrganizationExtension.php @@ -0,0 +1,25 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + $loader->load('datagrid.yml'); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php b/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php new file mode 100644 index 00000000000..18d8e2ea085 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php @@ -0,0 +1,389 @@ +id; + } + + /** + * Set name + * + * @param string $name + * @return BusinessUnit + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set parent + * + * @param BusinessUnit $parent + * @return BusinessUnit + */ + public function setParent(BusinessUnit $parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Get parent + * + * @return BusinessUnit + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set organization + * + * @param Organization $organization + * @return BusinessUnit + */ + public function setOrganization(Organization $organization) + { + $this->organization = $organization; + + return $this; + } + + /** + * Get organization + * + * @return Organization + */ + public function getOrganization() + { + return $this->organization; + } + + /** + * Set phone + * + * @param string $phone + * @return BusinessUnit + */ + public function setPhone($phone) + { + $this->phone = $phone; + + return $this; + } + + /** + * Get phone + * + * @return string + */ + public function getPhone() + { + return $this->phone; + } + + /** + * Set website + * + * @param string $website + * @return BusinessUnit + */ + public function setWebsite($website) + { + $this->website = $website; + + return $this; + } + + /** + * Get website + * + * @return string + */ + public function getWebsite() + { + return $this->website; + } + + /** + * Set email + * + * @param string $email + * @return BusinessUnit + */ + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + /** + * Get email + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set fax + * + * @param string $fax + * @return BusinessUnit + */ + public function setFax($fax) + { + $this->fax = $fax; + + return $this; + } + + /** + * Get fax + * + * @return string + */ + public function getFax() + { + return $this->fax; + } + + /** + * Get user created date/time + * + * @return DateTime + */ + public function getCreatedAt() + { + return $this->createdAt; + } + + /** + * Get user last update date/time + * + * @return DateTime + */ + public function getUpdatedAt() + { + return $this->updatedAt; + } + + /** + * Pre persist event handler + * + * @ORM\PrePersist + */ + public function prePersist() + { + $this->createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + $this->updatedAt = $this->createdAt; + } + + /** + * Pre update event handler + * + * @ORM\PreUpdate + */ + public function preUpdate() + { + $this->updatedAt = new \DateTime('now', new \DateTimeZone('UTC')); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getName(); + } + + /** + * @return ArrayCollection + */ + public function getUsers() + { + $this->users = $this->users ?: new ArrayCollection(); + + return $this->users; + } + + /** + * @param ArrayCollection $users + * @return BusinessUnit + */ + public function setUsers($users) + { + $this->users = $users; + + return $this; + } + + /** + * @param User $user + * @return BusinessUnit + */ + public function addUser(User $user) + { + if (!$this->getUsers()->contains($user)) { + $this->getUsers()->add($user); + } + + return $this; + } + + /** + * @param User $user + * @return BusinessUnit + */ + public function removeUser(User $user) + { + if ($this->getUsers()->contains($user)) { + $this->getUsers()->removeElement($user); + } + + return $this; + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php b/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php new file mode 100644 index 00000000000..6ff5757dc9c --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php @@ -0,0 +1,55 @@ +em = $em; + } + + /** + * Get Business Units tree + * + * @param User $entity + * @return array + */ + public function getBusinessUnitsTree(User $entity) + { + return $this->getBusinessUnitRepo()->getBusinessUnitsTree($entity); + } + + /** + * @param User $entity + * @param array $businessUnits + */ + public function assignBusinessUnits($entity, array $businessUnits) + { + if ($businessUnits) { + $businessUnits = $this->getBusinessUnitRepo()->getBusinessUnits($businessUnits); + } else { + $businessUnits = new ArrayCollection(); + } + $entity->setBusinessUnits($businessUnits); + } + + /** + * @return BusinessUnitRepository + */ + protected function getBusinessUnitRepo() + { + return $this->em->getRepository('OroOrganizationBundle:BusinessUnit'); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/Organization.php b/src/Oro/Bundle/OrganizationBundle/Entity/Organization.php new file mode 100644 index 00000000000..2b9d16c80cf --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Entity/Organization.php @@ -0,0 +1,131 @@ +id; + } + + /** + * Set name + * + * @param string $name + * @return Organization + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set Currency + * + * @param string $currency + * @return Organization + */ + public function setCurrency($currency) + { + $this->currency = $currency; + + return $this; + } + + /** + * Get Currency + * + * @return string + */ + public function getCurrency() + { + return $this->currency; + } + + /** + * Set Precision + * + * @param string $precision + * @return Organization + */ + public function setPrecision($precision) + { + $this->precision = $precision; + + return $this; + } + + /** + * Get Precision + * + * @return string + */ + public function getPrecision() + { + return $this->precision; + } + + /** + * @return string + */ + public function __toString() + { + return $this->getName(); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/Repository/BusinessUnitRepository.php b/src/Oro/Bundle/OrganizationBundle/Entity/Repository/BusinessUnitRepository.php new file mode 100644 index 00000000000..8c2cd0a096e --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Entity/Repository/BusinessUnitRepository.php @@ -0,0 +1,74 @@ +createQueryBuilder('businessUnit') + ->select( + array( + 'businessUnit.id', + 'businessUnit.name', + 'IDENTITY(businessUnit.parent) parent', + ) + ); + if ($user->getId()) { + $units = $user->getBusinessUnits()->map( + function (BusinessUnit $businessUnit) { + return $businessUnit->getId(); + } + ); + $units = $units->toArray(); + if ($units) { + $businessUnits->addSelect('CASE WHEN businessUnit.id IN (:userUnits) THEN 1 ELSE 0 END as hasUser'); + $businessUnits->setParameter(':userUnits', $units); + } + } + $businessUnits = $businessUnits->getQuery()->getArrayResult(); + $children = array(); + foreach ($businessUnits as &$businessUnit) { + $parent = $businessUnit['parent'] ?: 0; + $children[$parent][] = &$businessUnit; + } + unset($businessUnit); + foreach ($businessUnits as &$businessUnit) { + if (isset($children[$businessUnit['id']])) { + $businessUnit['children'] = $children[$businessUnit['id']]; + } + } + unset($businessUnit); + + if (isset($children[0])) { + $children = $children[0]; + } + + return $children; + } + + /** + * @param array $businessUnits + * @return mixed + */ + public function getBusinessUnits(array $businessUnits) + { + return $this->createQueryBuilder('businessUnit') + ->select('businessUnit') + ->where('businessUnit.id in (:ids)') + ->setParameter('ids', $businessUnits) + ->getQuery() + ->execute(); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Form/Handler/BusinessUnitHandler.php b/src/Oro/Bundle/OrganizationBundle/Form/Handler/BusinessUnitHandler.php new file mode 100644 index 00000000000..31209f6a627 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Form/Handler/BusinessUnitHandler.php @@ -0,0 +1,111 @@ +form = $form; + $this->request = $request; + $this->manager = $manager; + } + + /** + * Process form + * + * @param BusinessUnit $entity + * @return bool True on successfull processing, false otherwise + */ + public function process(BusinessUnit $entity) + { + $this->form->setData($entity); + + if (in_array($this->request->getMethod(), array('POST', 'PUT'))) { + $this->form->submit($this->request); + + if ($this->form->isValid()) { + $appendUsers = $this->form->get('appendUsers')->getData(); + $removeUsers = $this->form->get('removeUsers')->getData(); + $this->onSuccess($entity, $appendUsers, $removeUsers); + + return true; + } + } + + return false; + } + + /** + * "Success" form handler + * + * @param BusinessUnit $entity + * @param User[] $appendUsers + * @param User[] $removeUsers + */ + protected function onSuccess(BusinessUnit $entity, array $appendUsers, array $removeUsers) + { + $this->appendUsers($entity, $appendUsers); + $this->removeUsers($entity, $removeUsers); + $this->manager->persist($entity); + $this->manager->flush(); + } + + /** + * Append users to business unit + * + * @param BusinessUnit $businessUnit + * @param User[] $users + */ + protected function appendUsers(BusinessUnit $businessUnit, array $users) + { + /** @var $user User */ + foreach ($users as $user) { + $user->addBusinessUnit($businessUnit); + $this->manager->persist($user); + } + } + + /** + * Remove users from business unit + * + * @param BusinessUnit $businessUnit + * @param User[] $users + */ + protected function removeUsers(BusinessUnit $businessUnit, array $users) + { + /** @var $user User */ + foreach ($users as $user) { + $user->removeBusinessUnit($businessUnit); + $this->manager->persist($user); + } + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitApiType.php b/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitApiType.php new file mode 100644 index 00000000000..d7bf5686cf6 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitApiType.php @@ -0,0 +1,44 @@ +addEventSubscriber(new PatchSubscriber()); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + parent::setDefaultOptions($resolver); + + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\OrganizationBundle\Entity\BusinessUnit', + 'intention' => 'business_unit', + 'csrf_protection' => false + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'business_unit'; + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitType.php b/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitType.php new file mode 100644 index 00000000000..e96e61e9e32 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Form/Type/BusinessUnitType.php @@ -0,0 +1,110 @@ +add( + 'name', + 'text', + array( + 'required' => true, + ) + ) + ->add( + 'phone', + 'text', + array( + 'required' => false, + ) + ) + ->add( + 'website', + 'text', + array( + 'required' => false, + ) + ) + ->add( + 'email', + 'text', + array( + 'required' => false, + ) + ) + ->add( + 'fax', + 'text', + array( + 'required' => false, + ) + ) + ->add( + 'parent', + 'entity', + array( + 'label' => 'Parent Unit', + 'class' => 'OroOrganizationBundle:BusinessUnit', + 'property' => 'name', + 'required' => false, + 'multiple' => false, + ) + ) + ->add( + 'organization', + 'entity', + array( + 'label' => 'Organization', + 'class' => 'OroOrganizationBundle:Organization', + 'property' => 'name', + 'required' => true, + 'multiple' => false, + ) + ) + ->add( + 'appendUsers', + 'oro_entity_identifier', + array( + 'class' => 'OroUserBundle:User', + 'required' => false, + 'mapped' => false, + 'multiple' => true, + ) + ) + ->add( + 'removeUsers', + 'oro_entity_identifier', + array( + 'class' => 'OroUserBundle:User', + 'required' => false, + 'mapped' => false, + 'multiple' => true, + ) + ); + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\OrganizationBundle\Entity\BusinessUnit' + ) + ); + } + + public function getName() + { + return 'oro_business_unit'; + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/OroOrganizationBundle.php b/src/Oro/Bundle/OrganizationBundle/OroOrganizationBundle.php new file mode 100644 index 00000000000..82d7eee7d77 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/OroOrganizationBundle.php @@ -0,0 +1,9 @@ + +{% for businessUnit in businessUnits %} + {% set isChecked = hasId ? (businessUnit.hasUser is defined and businessUnit.hasUser == 1) : false%} + {% set hasChildren = businessUnit.children is defined and businessUnit.children %} +
    • + {{ businessUnit.name|trans }} + + {% if hasChildren %} + {% include 'OroOrganizationBundle:BusinessUnit:businessUnitsTree.html.twig' with {'businessUnits': businessUnit.children} %} + {% endif %} +
    • +{% endfor %} + diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/index.html.twig b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/index.html.twig new file mode 100644 index 00000000000..147160b384c --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/index.html.twig @@ -0,0 +1,11 @@ +{% extends 'OroUIBundle:actions:index.html.twig' %} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% set gridId = 'business-unit-grid' %} +{% block content %} + {% set pageTitle = 'Business Units' %} + {% set buttons = [] %} + {% if resource_granted('oro_business_unit_create') %} + {% set buttons = buttons|merge([UI.addButton({'path' : path('oro_business_unit_create'), 'title' : 'Create business unit', 'label' : 'Create business unit'})]) %} + {% endif %} + {{ parent() }} +{% endblock %} diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/update.html.twig b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/update.html.twig new file mode 100644 index 00000000000..a74570bf2ca --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/update.html.twig @@ -0,0 +1,104 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with ['OroFormBundle:Form:fields.html.twig', 'OroTagBundle:Form:fields.html.twig'] %} +{% set title = form.vars.value.id ? 'Update Business Unit'|trans : 'New Business Unit'|trans %} +{% oro_title_set({params : { "%business_unit.name%": form.vars.value.name } }) %} +{% set gridId = 'users-grid' %} +{% set formAction = form.vars.value.id ? path('oro_business_unit_update', { 'id': form.vars.value.id}) : path('oro_business_unit_create') %} +{% block head_script %} + {{ parent() }} + + {% set listenerParameters = { + 'columnName': 'has_business_unit', + 'selectors': { + 'included': '#businessUnitAppendUsers', + 'excluded': '#businessUnitRemoveUsers' + } + } %} + + {% placeholder prepare_grid with {'datagrid': datagrid, 'selector': '#' ~ gridId, 'parameters': listenerParameters} %} +{% endblock %} + +{% block navButtons %} + {% if form.vars.value.id and resource_granted('oro_business_unit_delete') %} + {{ UI.deleteButton({ + 'dataUrl': path('oro_api_delete_businessunit', {'id': form.vars.value.id}), + 'dataRedirect': path('oro_business_unit_index'), + 'aCss': 'no-hash remove-button', + 'dataId': form.vars.value.id, + 'id': 'btn-remove-business_unit', + 'dataMessage': 'Are you sure you want to delete this business unit?', + 'title': 'Delete business unit', + 'label': 'Delete' + }) + }} + {{ UI.buttonSeparator() }} + {% endif %} + {{ UI.button({'path' : path('oro_business_unit_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% if form.vars.value.id %} + {% set breadcrumbs = { + 'entity': form.vars.value, + 'indexPath': path('oro_business_unit_index'), + 'indexLabel': 'Business Units', + 'entityTitle': form.vars.value.name + } + %} + {{ parent() }} + {% else %} + {% include 'OroUIBundle::page_title_block.html.twig' %} + {% endif %} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'business_unit-profile' %} + + {% set dataBlocks = [{ + 'title': 'General', + 'class': 'active', + 'subblocks': [ + { + 'title': '', + 'data': [ + form_widget(form.appendUsers, {'id': 'businessUnitAppendUsers'}), + form_widget(form.removeUsers, {'id': 'businessUnitRemoveUsers'}), + form_row(form.name), + form_row(form.organization), + form_row(form.parent), + form_row(form.phone), + form_row(form.website), + form_row(form.email), + form_row(form.fax), + ] + } + ] + }] + %} + + {% set dataBlocks = dataBlocks|merge( + [{ + 'title' : 'Users', + 'subblocks': + [ + { + 'title' : null, + 'useSpan': false, + 'data' : [UI.gridBlock(gridId)] + } + ] + }] + ) + %} + + {% set data = + { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + } + %} + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/view.html.twig b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/view.html.twig new file mode 100644 index 00000000000..8cddebabc16 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Resources/views/BusinessUnit/view.html.twig @@ -0,0 +1,85 @@ +{% extends 'OroUIBundle:actions:view.html.twig' %} +{% oro_title_set({params : {"%business_unit.name%": entity.name|default('N/A') }}) %} +{% set gridId = 'users-grid' %} +{% block head_script %} + {% placeholder prepare_grid with {'datagrid': datagrid, 'selector': '#' ~ gridId} %} +{% endblock %} +{% set entityClass = 'Oro_Bundle_OrganizationBundle_Entity_BusinessUnit' %} + +{% block navButtons %} + {% if resource_granted('oro_business_unit_update') %} + {{ UI.button({'path' : path('oro_business_unit_update', { id: entity.id }), 'iClass' : 'icon-edit ', 'title' : 'Edit business_unit', 'label' : 'Edit'}) }} + {% endif %} + {% if resource_granted('oro_business_unit_delete') %} + {{ UI.deleteButton({ + 'dataUrl': path('oro_api_delete_businessunit', {'id': entity.id}), + 'dataRedirect': path('oro_business_unit_index'), + 'aCss': 'no-hash remove-button', + 'id': 'btn-remove-business_unit', + 'dataId': entity.id, + 'dataMessage': 'Are you sure you want to delete this business unit?', + 'title': 'Delete business unit', + 'label': 'Delete' + }) }} + {% endif %} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': entity, + 'indexPath': path('oro_business_unit_index'), + 'indexLabel': 'Business Units', + 'entityTitle': entity.name|default('N/A'), + } + %} + {{ parent() }} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'business-unit-profile' %} + + {% set dataSubBlocks = [] %} + + {% set dataBlocks = [{ + 'title': 'General', + 'class': 'active', + 'subblocks': [ + { + 'title': 'Basic Information', + 'data': [ + UI.attibuteRow('Name', entity.name), + UI.attibuteRow('Phone', entity.phone), + UI.attibuteRow('Organization', entity.organization), + UI.attibuteRow('Parent', entity.parent), + UI.attibuteRow('Website', entity.website), + UI.attibuteRow('Email', entity.email), + UI.attibuteRow('Fax', entity.fax), + ] + }, + ] + }] + %} + + {% set dataBlocks = dataBlocks|merge( + [{ + 'title' : 'Users', + 'subblocks': + [ + { + 'title' : null, + 'useSpan': false, + 'data' : [UI.gridBlock(gridId)] + } + ] + }] + ) + %} + + {% set data = + { + 'dataBlocks': dataBlocks, + } + %} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 00000000000..1e6fae9fd07 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,19 @@ +getConfigTreeBuilder(); + + $this->assertInstanceOf('Symfony\Component\Config\Definition\Builder\TreeBuilder', $builder); + + $root = $builder->buildTree(); + $this->assertInstanceOf('Symfony\Component\Config\Definition\ArrayNode', $root); + $this->assertEquals('oro_organization', $root->getName()); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/BusinessUnitTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/BusinessUnitTest.php new file mode 100644 index 00000000000..7dc2ac9b0c1 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/BusinessUnitTest.php @@ -0,0 +1,121 @@ +unit = new BusinessUnit(); + } + + public function testId() + { + $this->assertNull($this->unit->getId()); + } + + public function testName() + { + $name = 'test'; + $this->assertNull($this->unit->getName()); + $this->unit->setName($name); + $this->assertEquals($name, $this->unit->getName()); + $this->assertEquals($name, (string)$this->unit); + } + + public function testParent() + { + $parent = new BusinessUnit(); + $this->assertNull($this->unit->getParent()); + $this->unit->setParent($parent); + $this->assertEquals($parent, $this->unit->getParent()); + } + + public function testOrganization() + { + $organization = new Organization(); + $this->assertNull($this->unit->getOrganization()); + $this->unit->setOrganization($organization); + $this->assertEquals($organization, $this->unit->getOrganization()); + } + + public function testPhone() + { + $phone = 911; + $this->assertNull($this->unit->getPhone()); + $this->unit->setPhone($phone); + $this->assertEquals($phone, $this->unit->getPhone()); + } + + public function testWebsite() + { + $site = 'http://test.com'; + $this->assertNull($this->unit->getWebsite()); + $this->unit->setWebsite($site); + $this->assertEquals($site, $this->unit->getWebsite()); + } + + public function testEmail() + { + $mail = 'test@test.com'; + $this->assertNull($this->unit->getEmail()); + $this->unit->setEmail($mail); + $this->assertEquals($mail, $this->unit->getEmail()); + } + + public function testFax() + { + $fax = '321'; + $this->assertNull($this->unit->getFax()); + $this->unit->setFax($fax); + $this->assertEquals($fax, $this->unit->getFax()); + } + + public function testPrePersist() + { + $dateCreated = new \DateTime(); + $dateCreated = $dateCreated->format('yy'); + $this->assertNull($this->unit->getCreatedAt()); + $this->assertNull($this->unit->getUpdatedAt()); + $this->unit->prePersist(); + $this->assertEquals($dateCreated, $this->unit->getCreatedAt()->format('yy')); + $this->assertEquals($dateCreated, $this->unit->getUpdatedAt()->format('yy')); + } + + public function testUpdated() + { + $dateCreated = new \DateTime(); + $dateCreated = $dateCreated->format('yy'); + $this->assertNull($this->unit->getUpdatedAt()); + $this->unit->preUpdate(); + $this->assertEquals($dateCreated, $this->unit->getUpdatedAt()->format('yy')); + } + + public function testUser() + { + $businessUnit = new BusinessUnit(); + $user = new User(); + + $businessUnit->setUsers(new ArrayCollection(array($user))); + + $this->assertContains($user, $businessUnit->getUsers()); + + $businessUnit->removeUser($user); + + $this->assertNotContains($user, $businessUnit->getUsers()); + + $businessUnit->addUser($user); + + $this->assertContains($user, $businessUnit->getUsers()); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/OrganizationTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/OrganizationTest.php new file mode 100644 index 00000000000..7eaf67e403f --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Entity/OrganizationTest.php @@ -0,0 +1,47 @@ +organization = new Organization(); + } + + public function testName() + { + $name = 'testName'; + $this->assertNull($this->organization->getName()); + $this->organization->setName($name); + $this->assertEquals($name, $this->organization->getName()); + $this->assertEquals($name, (string)$this->organization); + } + + public function testId() + { + $this->assertNull($this->organization->getId()); + } + + public function testCurrency() + { + $currency = 'USD'; + $this->assertNull($this->organization->getCurrency()); + $this->organization->setCurrency($currency); + $this->assertEquals($currency, $this->organization->getCurrency()); + } + + public function testPrecision() + { + $precision = '000 000.00'; + $this->assertNull($this->organization->getPrecision()); + $this->organization->setPrecision($precision); + $this->assertEquals($precision, $this->organization->getPrecision()); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Handler/BusinessUnitHandlerTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Handler/BusinessUnitHandlerTest.php new file mode 100644 index 00000000000..4e42d8d5435 --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Handler/BusinessUnitHandlerTest.php @@ -0,0 +1,128 @@ +manager = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') + ->disableOriginalConstructor() + ->getMock(); + $this->request = new Request(); + $this->form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + + $this->entity = new BusinessUnit(); + $this->handler = new BusinessUnitHandler($this->form, $this->request, $this->manager); + } + + public function testProcessValidData() + { + $appendedUser = new User(); + $appendedUser->setId(1); + + $removedUser = new User(); + $removedUser->setId(2); + + $removedUser->addBusinessUnit($this->entity); + + $this->form->expects($this->once()) + ->method('setData') + ->with($this->entity); + + $this->form->expects($this->once()) + ->method('submit') + ->with($this->request); + + $this->request->setMethod('POST'); + + $this->form->expects($this->once()) + ->method('isValid') + ->will($this->returnValue(true)); + + $appendForm = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + $appendForm->expects($this->once()) + ->method('getData') + ->will($this->returnValue(array($appendedUser))); + $this->form->expects($this->at(3)) + ->method('get') + ->with('appendUsers') + ->will($this->returnValue($appendForm)); + + $removeForm = $this->getMockBuilder('Symfony\Component\Form\Form') + ->disableOriginalConstructor() + ->getMock(); + $removeForm->expects($this->once()) + ->method('getData') + ->will($this->returnValue(array($removedUser))); + $this->form->expects($this->at(4)) + ->method('get') + ->with('removeUsers') + ->will($this->returnValue($removeForm)); + + $this->manager->expects($this->at(0)) + ->method('persist') + ->with($appendedUser); + + $this->manager->expects($this->at(1)) + ->method('persist') + ->with($removedUser); + + $this->manager->expects($this->at(2)) + ->method('persist') + ->with($this->entity); + + $this->manager->expects($this->once()) + ->method('flush'); + + $this->assertTrue($this->handler->process($this->entity)); + + $businessUnits = $appendedUser->getBusinessUnits()->toArray(); + $this->assertCount(1, $businessUnits); + $this->assertEquals($this->entity, current($businessUnits)); + $this->assertCount(0, $removedUser->getBusinessUnits()->toArray()); + } + + public function testBadMethod() + { + $this->request->setMethod('GET'); + $this->assertFalse($this->handler->process($this->entity)); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Type/BusinessUnitTypeTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Type/BusinessUnitTypeTest.php new file mode 100644 index 00000000000..b26b01895ac --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Form/Type/BusinessUnitTypeTest.php @@ -0,0 +1,45 @@ +form = new BusinessUnitType(); + } + + public function testSetDefaultOptions() + { + $optionResolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + + $optionResolver->expects($this->once()) + ->method('setDefaults') + ->with(array('data_class' => 'Oro\Bundle\OrganizationBundle\Entity\BusinessUnit')); + $this->form->setDefaultOptions($optionResolver); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + $builder->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); + + $this->form->buildForm($builder, array()); + } + + public function testGetName() + { + $this->assertEquals('oro_business_unit', $this->form->getName()); + } +} diff --git a/src/Oro/Bundle/OrganizationBundle/composer.json b/src/Oro/Bundle/OrganizationBundle/composer.json new file mode 100644 index 00000000000..731e0821e3d --- /dev/null +++ b/src/Oro/Bundle/OrganizationBundle/composer.json @@ -0,0 +1,23 @@ +{ + "name": "oro/organization-bundle", + "type": "symfony-bundle", + "description": "BAP Organization Bundle", + "keywords": ["BAP"], + "license": "MIT", + "require": { + "php": ">=5.3.3", + "symfony/symfony": "2.1.*", + "oro/ui-bundle": "dev-master", + "oro/user-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\OrganizationBundle": "" } + }, + "target-dir": "Oro/Bundle/OrganizationBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Oro/Bundle/SearchBundle/Resources/config/navigation.yml b/src/Oro/Bundle/SearchBundle/Resources/config/navigation.yml index 4a4be42790d..1c3689155fa 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/SearchBundle/Resources/config/navigation.yml @@ -1,3 +1,3 @@ oro_titles: oro_search_advanced: Dashboard | Advanced Search - oro_search_results: Search results - %%keyword%% + oro_search_results: Search Results - %%keyword%% diff --git a/src/Oro/Bundle/SearchBundle/Resources/public/js/searchBar.js b/src/Oro/Bundle/SearchBundle/Resources/public/js/searchBar.js index 5928a73aed1..19a2c1c8d16 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/public/js/searchBar.js +++ b/src/Oro/Bundle/SearchBundle/Resources/public/js/searchBar.js @@ -1,5 +1,6 @@ $(document).ready(function () { var _searchFlag = false; + var timeout = 700; var searchBarContainer = $('#search-div'); var searchBarInput = searchBarContainer.find('#search-bar-search'); var searchBarDropdown = searchBarContainer.find('#search-bar-dropdown'); @@ -10,12 +11,7 @@ $(document).ready(function () { if (!_.isUndefined(Oro.Events)) { Oro.Events.bind( 'hash_navigation_request:complete', - function() { - SearchByTagClose(); - if (searchBarInput.size()) { - SearchInputWidth(); - } - }, + SearchByTagClose, this ); @@ -52,11 +48,12 @@ $(document).ready(function () { searchBarForm.val($(this).parent().attr('data-alias')); searchBarButton.find('.search-bar-type').html($(this).html()); SearchByTagClose(); - SearchInputWidth(); e.preventDefault(); }); + var searchInterval = null; function SearchByTag() { + clearInterval(searchInterval); var queryString = searchBarInput.val(); if (queryString == '' || queryString.length < 3) { @@ -90,16 +87,6 @@ $(document).ready(function () { } }); } - }; - - function SearchInputWidth() { - var _generalWidth = searchBarContainer.width(); - var searchBtnWidth = searchBarContainer.find('.btn-search').outerWidth(); - var searchBarButtonWidth = searchBarButton.outerWidth(); - - /* just need a design without border */ - searchBarInput.width(_generalWidth - (searchBtnWidth + searchBarButtonWidth)); - searchDropdown.width(_generalWidth - searchBarButtonWidth + 8); } function SearchByTagClose() { @@ -121,17 +108,22 @@ $(document).ready(function () { } }); - searchBarInput.keyup(function(event) { - switch(event.keyCode) { - case 40: //down - case 38: //up - searchBarContainer.addClass('header-search-focused'); - searchDropdown.find('a:first').focus(); - event.preventDefault(); - - return false; - default: - SearchByTag(); + searchBarInput.keypress(function(e) { + if (e.keyCode == 8 || e.keyCode == 46 || (e.which !== 0 && e.charCode !== 0 && !e.ctrlKey && !e.altKey)) { + clearInterval(searchInterval); + searchInterval = setInterval(SearchByTag, timeout); + } else { + switch (e.keyCode) { + case 40: + case 38: + searchBarContainer.addClass('header-search-focused'); + searchDropdown.find('a:first').focus(); + e.preventDefault(); + return false; + case 27: + searchBarContainer.removeClass('header-search-focused'); + break; + } } }); @@ -175,6 +167,10 @@ $(document).ready(function () { case 40: // Down arrow selectNext(); break; + case 27: + searchBarContainer.removeClass('header-search-focused'); + searchBarInput.focus(); + break; } }); diff --git a/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchBar.html.twig b/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchBar.html.twig index 7176c8e59f5..781f7cc9294 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchBar.html.twig +++ b/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchBar.html.twig @@ -1,26 +1,32 @@ -
      -
      \ <% } %>\ \ \ '), diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/form_buttons.js b/src/Oro/Bundle/UIBundle/Resources/public/js/form_buttons.js new file mode 100644 index 00000000000..3bbd1a71688 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/form_buttons.js @@ -0,0 +1,7 @@ +$(document).ready(function () { + $(document).on('click', '.action-button', function () { + var actionInput = $('input[name = "input_action"]'); + actionInput.val($(this).attr('data-action')); + $('#' + actionInput.attr('data-form-id')).submit(); + }); +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js index f9b837c43cc..e4b771418e5 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js @@ -125,6 +125,10 @@ $(document).ready(function () { $('.open:not(._currently_clicked)').removeClass('open') clickingTarget.removeClass('_currently_clicked'); }); + + $('#main-menu').mouseover(function() { + $('.open').removeClass('open'); + }) }); function hideProgressBar() { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/remove.confirm.js b/src/Oro/Bundle/UIBundle/Resources/public/js/remove.confirm.js index 769c28fa45f..ef6cac00edf 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/remove.confirm.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/remove.confirm.js @@ -18,9 +18,9 @@ $(function() { if (!_.isUndefined(Oro.BootstrapModal)) { var confirm = new Oro.BootstrapModal({ - title: 'Delete Confirmation', + title: _.__('Delete Confirmation'), content: message, - okText: 'Yes, Delete' + okText: _.__('Yes, Delete') }); confirm.on('ok', doAction); confirm.open(); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.min.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.min.js index d78d00fc406..1537edca041 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.min.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.min.js @@ -1,7 +1,7 @@ /* Copyright 2012 Igor Vaynberg -Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 +Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU General Public License version 2 (the "GPL License"). You may choose either license to govern your @@ -18,5 +18,5 @@ or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CO either express or implied. See the Apache License and the GPL License for the specific language governing permissions and limitations under the Apache License and the GPL License. */ -(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function m(a,b){for(var c=0,d=b.length;d>c;c+=1)if(o(a,b[c]))return c;return-1}function n(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function o(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function p(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function q(a){return a.outerWidth(!1)-a.width()}function r(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function s(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function t(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function u(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function v(a,b){var c=t(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){m(a.target,b.get())>=0&&c(a)})}function w(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function x(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function y(a){a.preventDefault(),a.stopPropagation()}function z(a){a.preventDefault(),a.stopImmediatePropagation()}function A(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function B(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function C(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push(""),d.push(e(a.substring(f,f+g))),d.push(""),d.push(e(a.substring(f+g,a.length))),b)}function D(c){var d,e=0,f=null,g=c.quietMillis||100,h=c.url,i=this;return function(j){window.clearTimeout(d),d=window.setTimeout(function(){e+=1;var d=e,g=c.data,k=h,l=c.transport||a.fn.select2.ajaxDefaults.transport,m={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},n=a.extend({},a.fn.select2.ajaxDefaults.params,m);g=g?g.call(i,j.term,j.page,j.context):null,k="function"==typeof k?k.call(i,j.term,j.page,j.context):k,null!==f&&f.abort(),c.params&&(a.isFunction(c.params)?a.extend(n,c.params.call(i)):a.extend(n,c.params)),a.extend(n,{url:k,dataType:c.dataType,data:g,success:function(a){if(!(e>d)){var b=c.results(a,j.page);j.callback(b)}}}),f=l.call(i,n)},g)}}function E(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=h.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function F(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function G(b){if(a.isFunction(b))return!0;if(!b)return!1;throw Error("formatterName must be a function or a falsy value")}function H(b){return a.isFunction(b)?b():b}function I(b){var c=0;return a.each(b,function(a,b){b.children?c+=I(b.children):c++}),c}function J(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice(h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(o(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function K(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,i,j,k,c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="
      ";j=a(document),g=function(){var a=1;return function(){return a++}}(),j.on("mousemove",function(a){i={x:a.pageX,y:a.pageY}}),d=K(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,h,i,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&this.destroy(),this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=u(function(){return c.element.closest("body")}),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(H(c.containerCss)),this.container.addClass(H(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(H(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.resultsPage=0,this.context=null,this.initContainer(),s(this.results),this.dropdown.on("mousemove-filtered touchstart touchmove touchend",f,this.bind(this.highlightUnderEvent)),v(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),y(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),y(a))}),r(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||n(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),a!==b&&(a.container.remove(),a.dropdown.remove(),a.opts.element.removeClass("select2-offscreen").removeData("select2").off(".select2").attr({tabindex:this.elementTabIndex}).prop("autofocus",this.autofocus||!1).show())},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:o(a.attr("locked"),"locked")}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:b},prepareOpts:function(c){var d,e,f,g,h=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a ","
      "," ","
        ","
      ","
      "].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var b,c;this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.search.focus(),b=this.search.get(0),b.createTextRange&&(c=b.createTextRange(),c.collapse(!1),c.select()),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown;this.showSearch(!1),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.focusser.attr("tabindex",this.elementTabIndex),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return y(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){return!this.isInterfaceEnabled()||a.which===c.TAB||c.isControl(a)||c.isFunctionKey(a)||a.which===c.ESC?b:this.opts.openOnEnter===!1&&a.which===c.ENTER?(y(a),b):a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter?(this.open(),y(a),b):a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),y(a),b):b})),r(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){a.stopPropagation(),this.opened()||this.open()})),d.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),z(a),this.close(),this.selection.focus())})),d.on("mousedown",this.bind(function(b){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),y(b)})),f.on("mousedown",this.bind(function(){this.search.focus()})),d.on("focus",this.bind(function(a){y(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");b&&(this.opts.element.val(""),this.selection.find("span").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b})))},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text())this.updateSelection([]),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find(":selected");b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=o(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&""!==this.select.find("option").first().text()?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(""===this.opts.element.val()&&a!==b){if(this.select&&""!==this.select.find("option:first").text())return;this.selection.find("span").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,c,d){var e=0,f=this;if(this.findHighlightableChoices().each2(function(a,c){return o(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&this.highlight(e),c===!0&&this.showSearchInput===!1){var h=this.opts.minimumResultsForSearch;h>=0&&this.showSearch(I(a.results)>=h)}},showSearch:function(b){this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b)},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),o(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,c=this.selection.find("span");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c),d!==b&&c.append(this.opts.escapeMarkup(d)),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");if(!a&&0!==a)return this.clear(c),b;this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a,c){var d;return 0===arguments.length?(d=this.selection.data("select2-data"),d==b&&(d=null),d):(a&&""!==a?(d=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),c&&this.triggerChange({added:a,removed:d})):this.clear(c),b)}}),f=K(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["
        ","
      • "," ","
      • ","
      ","
      ","
        ","
      ","
      "].join("")); -return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find(":selected").each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=p(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return o(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;e.length>c;c++)for(var g=e[c],h=0;f.length>h;h++){var i=f[h];if(o(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d);var f=this;this.selection.on("mousedown",".select2-search-choice",function(){f.search[0].focus(),f.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var d=e.find(".select2-search-choice-focus"),f=d.prev(".select2-search-choice:not(.select2-locked)"),g=d.next(".select2-search-choice:not(.select2-locked)"),h=x(this.search);if(d.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var i=d;return a.which==c.LEFT&&f.length?i=f:a.which==c.RIGHT?i=g.length?g:null:a.which===c.BACKSPACE?(this.unselect(d.first()),this.search.width(10),i=f.length?f:g):a.which==c.DELETE?(this.unselect(d.first()),this.search.width(10),i=g.length?g:null):a.which==c.ENTER&&(i=null),this.selectChoice(i),y(a),i&&i.length||this.open(),b}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==h.offset&&!h.length)return this.selectChoice(e.find(".select2-search-choice:not(.select2-locked)").last()),y(a),b;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&y(a),a.which===c.ENTER&&y(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("mousedown",d,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",d,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.updateResults(!0),this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>m(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer(a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,d=!c.locked,e=a("
    • "),f=a("
    • "),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div")),j!=b&&g.find("div").replaceWith("
      "+this.opts.escapeMarkup(j)+"
      "),d&&g.find(".select2-search-choice-close").on("mousedown",y).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),y(b))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=m(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));m(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&this.results.append("
    • "+g.opts.formatNoMatches(g.search.val())+"
    • ")},getMaxSearchWidth:function(){return this.selection.width()-q(this.search)},resizeSearch:function(){var a,b,c,d,e,f=q(this.search);a=A(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),p(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>m(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;b.length>c;c++)for(var d=0;a.length>d;d++)o(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a(b).map(f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(this.buildChangeDetails(e,this.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c,d){var f,g,e=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(g=this.data(),c||(c=[]),f=a.map(c,function(a){return e.opts.id(a)}),this.setVal(f),this.updateSelection(c),this.clearSearch(),d&&this.triggerChange(this.buildChangeDetails(g,this.data())),b)}}),a.fn.select2=function(){var d,g,h,i,c=Array.prototype.slice.call(arguments,0),j=["val","destroy","opened","open","close","focus","isFocused","container","onSortStart","onSortEnd","enable","readonly","positionDropdown","data"],k=["val","opened","isFocused","container","data"];return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?i=d.element.prop("multiple"):(i=d.multiple||!1,"tags"in d&&(d.multiple=i=!0)),g=i?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>m(c[0],j))throw"Unknown method: "+c[0];if(h=b,g=a(this).data("select2"),g===b)return;if(h="container"===c[0]?g.container:g[c[0]].apply(g,c.slice(1)),m(c[0],k)>=0)return!1}}),h===b?this:h},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return C(a.text,c.term,e,d),e.join("")},formatSelection:function(a){return a?a.text:b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:J,escapeMarkup:function(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a]})},blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:D,local:E,tags:F},util:{debounce:t,markMatch:C},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file +(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function m(a,b){for(var c=0,d=b.length;d>c;c+=1)if(o(a,b[c]))return c;return-1}function n(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function o(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function p(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function q(a){return a.outerWidth(!1)-a.width()}function r(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function s(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function t(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function u(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function v(a,b){var c=t(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){m(a.target,b.get())>=0&&c(a)})}function w(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function x(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function y(a){a.preventDefault(),a.stopPropagation()}function z(a){a.preventDefault(),a.stopImmediatePropagation()}function A(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function B(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function C(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push(""),d.push(e(a.substring(f,f+g))),d.push(""),d.push(e(a.substring(f+g,a.length))),b)}function D(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function E(c){var d,e=0,f=null,g=c.quietMillis||100,h=c.url,i=this;return function(j){window.clearTimeout(d),d=window.setTimeout(function(){e+=1;var d=e,g=c.data,k=h,l=c.transport||a.fn.select2.ajaxDefaults.transport,m={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},n=a.extend({},a.fn.select2.ajaxDefaults.params,m);g=g?g.call(i,j.term,j.page,j.context):null,k="function"==typeof k?k.call(i,j.term,j.page,j.context):k,f&&f.abort(),c.params&&(a.isFunction(c.params)?a.extend(n,c.params.call(i)):a.extend(n,c.params)),a.extend(n,{url:k,dataType:c.dataType,data:g,success:function(a){if(!(e>d)){var b=c.results(a,j.page);j.callback(b)}}}),f=l.call(i,n)},g)}}function F(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=h.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function G(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function H(b,c){if(a.isFunction(b))return!0;if(!b)return!1;throw Error(c+" must be a function or a falsy value")}function I(b){return a.isFunction(b)?b():b}function J(b){var c=0;return a.each(b,function(a,b){b.children?c+=J(b.children):c++}),c}function K(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(o(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function L(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,j,k,i={x:0,y:0},c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="
      ";j=a(document),g=function(){var a=1;return function(){return a++}}(),j.on("mousemove",function(a){i.x=a.pageX,i.y=a.pageY}),d=L(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,h,i,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=u(function(){return c.element.closest("body")}),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(I(c.containerCss)),this.container.addClass(I(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(I(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.resultsPage=0,this.context=null,this.initContainer(),s(this.results),this.dropdown.on("mousemove-filtered touchstart touchmove touchend",f,this.bind(this.highlightUnderEvent)),v(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),y(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),y(a))}),r(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||n(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element,c=a.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),c!==b&&(c.container.remove(),c.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show())},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:o(a.attr("locked"),"locked")||o(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:b},prepareOpts:function(c){var d,e,f,g,h=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a ","
      "," ","
        ","
      ","
      "].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var b,c,d;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.search.focus(),b=this.search.get(0),b.createTextRange?(c=b.createTextRange(),c.collapse(!1),c.select()):b.setSelectionRange&&(d=this.search.val().length,b.setSelectionRange(d,d)),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown;0>this.opts.minimumResultsForSearch?this.showSearch(!1):this.showSearch(!0),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.focusser.attr("tabindex",this.elementTabIndex),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return y(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.ESC){if(this.opts.openOnEnter===!1&&a.which===c.ENTER)return y(a),b;if(a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),y(a),b}return a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),y(a),b):b}})),r(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),d.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),z(a),this.close(),this.selection.focus())})),d.on("mousedown",this.bind(function(b){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),y(b)})),f.on("mousedown",this.bind(function(){this.search.focus()})),d.on("focus",this.bind(function(a){y(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");if(b){var c=this.getPlaceholderOption();this.opts.element.val(c?c.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection([]),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},isPlaceholderOptionSelected:function(){var a;return(a=this.getPlaceholderOption())!==b&&a.is(":selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find(":selected");b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=o(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,c,d){var e=0,f=this;if(this.findHighlightableChoices().each2(function(a,c){return o(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&(c===!0&&e>=0?this.highlight(e):this.highlight(0)),c===!0){var h=this.opts.minimumResultsForSearch;h>=0&&this.showSearch(J(a.results)>=h)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),o(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c,this.opts.escapeMarkup),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear") +},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),b;if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a,c){var d;return 0===arguments.length?(d=this.selection.data("select2-data"),d==b&&(d=null),d):(a&&""!==a?(d=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),c&&this.triggerChange({added:a,removed:d})):this.clear(c),b)}}),f=L(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["
        ","
      • "," ","
      • ","
      ","
      ","
        ","
      ","
      "].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find(":selected").each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=p(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return o(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;e.length>c;c++)for(var g=e[c],h=0;f.length>h;h++){var i=f[h];if(o(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d);var f=this;this.selection.on("mousedown",".select2-search-choice",function(){f.search[0].focus(),f.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var d=e.find(".select2-search-choice-focus"),f=d.prev(".select2-search-choice:not(.select2-locked)"),g=d.next(".select2-search-choice:not(.select2-locked)"),h=x(this.search);if(d.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var i=d;return a.which==c.LEFT&&f.length?i=f:a.which==c.RIGHT?i=g.length?g:null:a.which===c.BACKSPACE?(this.unselect(d.first()),this.search.width(10),i=f.length?f:g):a.which==c.DELETE?(this.unselect(d.first()),this.search.width(10),i=g.length?g:null):a.which==c.ENTER&&(i=null),this.selectChoice(i),y(a),i&&i.length||this.open(),b}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==h.offset&&!h.length)return this.selectChoice(e.find(".select2-search-choice:not(.select2-locked)").last()),y(a),b;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),this.close(),b;case c.ESC:return this.cancel(a),y(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&y(a),a.which===c.ENTER&&y(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("click",d,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",d,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.updateResults(!0),this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>m(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("
    • "),f=a("
    • "),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith("
      "+j+"
      "),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",y).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),y(b))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=m(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));m(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&H(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("
    • "+g.opts.formatNoMatches(g.search.val())+"
    • ")},getMaxSearchWidth:function(){return this.selection.width()-q(this.search)},resizeSearch:function(){var a,b,c,d,e,f=q(this.search);a=A(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),p(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>m(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;b.length>c;c++)for(var d=0;a.length>d;d++)o(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(this.buildChangeDetails(e,this.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c,d){var f,g,e=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(g=this.data(),c||(c=[]),f=a.map(c,function(a){return e.opts.id(a)}),this.setVal(f),this.updateSelection(c),this.clearSearch(),d&&this.triggerChange(this.buildChangeDetails(g,this.data())),b)}}),a.fn.select2=function(){var d,g,h,i,j,c=Array.prototype.slice.call(arguments,0),k=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","readonly","positionDropdown","data","search"],l=["val","opened","isFocused","container","data"],n={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?j=d.element.prop("multiple"):(j=d.multiple||!1,"tags"in d&&(d.multiple=j=!0)),g=j?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>m(c[0],k))throw"Unknown method: "+c[0];if(i=b,g=a(this).data("select2"),g===b)return;if(h=c[0],"container"===h?i=g.container:"dropdown"===h?i=g.dropdown:(n[h]&&(h=n[h]),i=g[h].apply(g,c.slice(1))),m(c[0],l)>=0)return!1}}),i===b?this:i},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return C(a.text,c.term,e,d),e.join("")},formatSelection:function(a,c,d){return a?d(a.text):b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatSelectionCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:K,escapeMarkup:D,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:E,local:F,tags:G},util:{debounce:t,markMatch:C,escapeMarkup:D},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file diff --git a/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..6eeeda46538 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,2 @@ +"Delete Confirmation": "Delete Confirmation" +"Yes, Delete": "Yes, Delete" diff --git a/src/Oro/Bundle/UIBundle/Resources/views/Default/index.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/Default/index.html.twig index e18978eebd7..79cda94e637 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/Default/index.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/Default/index.html.twig @@ -15,9 +15,7 @@ {% block script %} {% placeholder scripts_before %} - {# Add translations, include only common domains translations until BAP-768 is not implemented #} - - + @@ -60,11 +58,22 @@
      {% block header %} {% endblock header %} {% block main %} @@ -87,15 +97,14 @@ {% block left_panel %} {% placeholder left_panel %} {% endblock left_panel %} - {% block pin_bar %} - {% placeholder pinbar %} - {% block application_menu %} - - {% endblock application_menu %} +
      {% block messages %} {% set hasMessages = app.session.flashbag.peekAll|length > 0 %} @@ -178,7 +187,8 @@ with { 'script': block('head_script'), 'content': block('page_container'), - 'menu': block('application_menu') + 'menu': block('application_menu'), + 'breadcrumb': block('breadcrumb') } %} {% endif %} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/Default/logo.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/Default/logo.html.twig index c30d3a23087..007d6c4a03c 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/Default/logo.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/Default/logo.html.twig @@ -1,4 +1,4 @@ -

      +

      {{ oro_config_value('oro_ui.application_name') }} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/Form/fields.html.twig index c57a5d21fb8..570befa8d66 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/Form/fields.html.twig @@ -193,3 +193,9 @@

      {% endspaceless %} {% endblock %} + +{% block _oro_entity_config_config_field_type_widget %} + {% for field in form.children %} + {{ form_widget(field) }} + {% endfor %} +{% endblock %} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/Form/translateable.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/Form/translateable.html.twig new file mode 100644 index 00000000000..288693d904e --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/views/Form/translateable.html.twig @@ -0,0 +1,57 @@ +{% block a2lix_translations_widget %} +
      + + +
      + {% for translationsFields in form %} + {% set locale = translationsFields.vars.name %} + +
      + {{ form_widget(translationsFields) }} +
      + {% endfor %} +
      +
      +{% endblock %} + +{% block a2lix_translations_gedmo_widget %} +
      + + +
      + {% for translationsLocales in form %} + {% for translationsFields in translationsLocales %} + {% set locale = translationsFields.vars.name %} + +
      + {{ form_widget(translationsFields) }} +
      + {% endfor %} + {% endfor %} +
      +
      +{% endblock %} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/actions/update.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/actions/update.html.twig index 8a13906b625..016042c7e30 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/actions/update.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/actions/update.html.twig @@ -12,9 +12,10 @@
      {% block navButtons %} {% endblock navButtons %} +
      {% if (entityClass is defined and form.vars.value.id) %} - {% placeholder change_history_block with {'entity': entityClass, 'id': form.vars.value.id, 'title': form.vars.value } %} + {% placeholder change_history_block with {'entity': entityClass, 'id': form.vars.value.id, 'title': audit_title|default(form.vars.value), 'audit_path': audit_path|default('oro_dataaudit_history') } %} {% endif %}
      @@ -31,6 +32,12 @@
      / + {% if breadcrumbs.additional is defined %} + {% for breadcrumb in breadcrumbs.additional %} + + / + {% endfor %} + {% endif %}

      {{ breadcrumbs.entityTitle }}

      {% endblock breadcrumbs %} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/actions/view.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/actions/view.html.twig index 54a9e0649d2..ab38666ceea 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/actions/view.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/actions/view.html.twig @@ -11,7 +11,7 @@ {% endblock navButtons %}
      {% if (entity is defined and entityClass is defined) %} - {% placeholder change_history_block with {'entity': entityClass, 'id': entity.id, 'title': entity } %} + {% placeholder change_history_block with {'entity': entityClass, 'id': entity.id, 'title': audit_title|default(entity), 'audit_path': audit_path|default('oro_dataaudit_history') } %} {% endif %}
      @@ -28,6 +28,12 @@
      / + {% if breadcrumbs.additional is defined %} + {% for breadcrumb in breadcrumbs.additional %} + + / + {% endfor %} + {% endif %}

      {{ breadcrumbs.entityTitle }}

      {% endblock breadcrumbs %} diff --git a/src/Oro/Bundle/UIBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/UIBundle/Resources/views/macros.html.twig index a1985e25b23..2bf7117d8eb 100644 --- a/src/Oro/Bundle/UIBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/UIBundle/Resources/views/macros.html.twig @@ -14,13 +14,18 @@ {% set form = widget %} {% set name = widget.vars.full_name %} {% endif %} +
      {{ form_errors(form) }} - {% for child in form %} - {{ form_errors(child) }} - {{ form_widget(child) }} - {% endfor %} + {% if form.children|length %} + {% for child in form %} + {{ form_errors(child) }} + {{ form_widget(child) }} + {% endfor %} + {% else %} + {{ form_widget(form) }} + {% endif %} {{ form_rest(form) }}
      @@ -165,12 +170,21 @@ #} {% macro buttonType(parameters) %}
      -
      {% endmacro %} +{% macro saveAndCloseButton(label = 'Save and Close') %} + {{ _self.buttonType({'type': 'submit', 'class': 'btn-success', 'label': label}) }} +{% endmacro %} + +{% macro saveAndStayButton(label = 'Save') %} + {{ _self.buttonType({'type': 'button', 'class': 'btn-success', 'label': label, 'action': 'save_and_stay'}) }} +{% endmacro %} + {# Separator between buttons #} diff --git a/src/Oro/Bundle/UIBundle/Route/Router.php b/src/Oro/Bundle/UIBundle/Route/Router.php new file mode 100644 index 00000000000..f693e0411dd --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Route/Router.php @@ -0,0 +1,67 @@ +request = $request; + $this->router = $router; + } + + /** + * "Save and Stay" and "Save and Close" buttons handler + * + * @param array $saveButtonRoute array with router data for save and stay button + * @param array $saveAndCloseRoute array with router data for save and close button + * @param int $status redirect status + * + * @return RedirectResponse + * @throws \LogicException + */ + public function actionRedirect(array $saveButtonRoute, array $saveAndCloseRoute, $status = 302) + { + if ($this->request->get(self::ACTION_PARAMETER) == self::ACTION_SAVE_AND_STAY) { + $routeData = $saveButtonRoute; + } else { + $routeData = $saveAndCloseRoute; + } + + if (!isset($routeData['route'])) { + throw new \LogicException('Parameter "route" is not defined.'); + } else { + $routeName = $routeData['route']; + } + + $params = isset($routeData['parameters']) ? $routeData['parameters'] : array(); + + return new RedirectResponse( + $this->router->generate( + $routeName, + $params, + UrlGeneratorInterface::ABSOLUTE_PATH + ), + $status + ); + } +} diff --git a/src/Oro/Bundle/UIBundle/Tests/Unit/Route/RouterTest.php b/src/Oro/Bundle/UIBundle/Tests/Unit/Route/RouterTest.php new file mode 100644 index 00000000000..f7003e9886f --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Tests/Unit/Route/RouterTest.php @@ -0,0 +1,81 @@ +request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $this->route = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Routing\Router') + ->disableOriginalConstructor() + ->getMock(); + $this->router = new Router($this->request, $this->route); + } + + public function testActionRedirect() + { + $testUrl = 'test\\url\\index.html'; + + $this->request->expects($this->once()) + ->method('get') + ->will($this->returnValue(Router::ACTION_SAVE_AND_STAY)); + + $this->route->expects($this->once()) + ->method('generate') + ->will($this->returnValue($testUrl)); + + $redirect = $this->router->actionRedirect( + array( + 'route' => 'test_route', + 'parameters' => array('id' => 1), + ), + array() + ); + + $this->assertEquals($testUrl, $redirect->getTargetUrl()); + } + + public function testSaveAndCloseActionRedirect() + { + $testUrl = 'save_and_close.html'; + + $this->request->expects($this->once()) + ->method('get') + ->will($this->returnValue('test')); + + $this->route->expects($this->once()) + ->method('generate') + ->will($this->returnValue($testUrl)); + /** + * @var \Symfony\Component\HttpFoundation\RedirectResponse + */ + $redirect = $this->router->actionRedirect( + array(), + array( + 'route' => 'test_route', + 'parameters' => array('id' => 1), + ) + ); + + $this->assertEquals($testUrl, $redirect->getTargetUrl()); + } + + public function testWrongParametersActionRedirect() + { + $this->setExpectedException('\LogicException'); + $this->router->actionRedirect( + array(), + array() + ); + } +} diff --git a/src/Oro/Bundle/UIBundle/Twig/Node/PlaceholderNode.php b/src/Oro/Bundle/UIBundle/Twig/Node/PlaceholderNode.php index 50070cf962b..4c482ff679e 100644 --- a/src/Oro/Bundle/UIBundle/Twig/Node/PlaceholderNode.php +++ b/src/Oro/Bundle/UIBundle/Twig/Node/PlaceholderNode.php @@ -27,7 +27,8 @@ class PlaceholderNode extends \Twig_Node */ public function __construct(array $placeholder, $variables, $wrapClassName, $line, $tag) { - parent::__construct(array(), array('value' => $placeholder['items']), $line, $tag); + $items = isset($placeholder['items']) ?: array(); + parent::__construct(array(), array('value' => $items), $line, $tag); $this->placeholder = $placeholder; $this->wrapClassName = $wrapClassName; $this->variables = $variables; diff --git a/src/Oro/Bundle/UserBundle/Acl/AclManipulator.php b/src/Oro/Bundle/UserBundle/Acl/AclManipulator.php index e5437eb78f6..0204e7e9581 100644 --- a/src/Oro/Bundle/UserBundle/Acl/AclManipulator.php +++ b/src/Oro/Bundle/UserBundle/Acl/AclManipulator.php @@ -54,8 +54,6 @@ public function __construct( $this->cache = $cache; $this->securityContext = $securityContext; $this->configReader = $configReader; - - $this->cache->setNamespace('oro_user.cache'); } /** diff --git a/src/Oro/Bundle/UserBundle/Controller/GroupController.php b/src/Oro/Bundle/UserBundle/Controller/GroupController.php index 9864e3f6483..37a6874790d 100644 --- a/src/Oro/Bundle/UserBundle/Controller/GroupController.php +++ b/src/Oro/Bundle/UserBundle/Controller/GroupController.php @@ -58,7 +58,16 @@ public function updateAction(Group $entity) $this->get('session')->getFlashBag()->add('success', 'Group successfully saved'); if (!$this->getRequest()->get('_widgetContainer')) { - return $this->redirect($this->generateUrl('oro_user_group_index')); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_user_group_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_user_group_index', + ) + ); } } diff --git a/src/Oro/Bundle/UserBundle/Controller/RoleController.php b/src/Oro/Bundle/UserBundle/Controller/RoleController.php index a2850e7f0ba..ba83e23dccb 100644 --- a/src/Oro/Bundle/UserBundle/Controller/RoleController.php +++ b/src/Oro/Bundle/UserBundle/Controller/RoleController.php @@ -60,7 +60,15 @@ public function updateAction(Role $entity) $this->get('session')->getFlashBag()->add('success', 'Role successfully saved'); - return $this->redirect($this->generateUrl('oro_user_role_index')); + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_user_role_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_user_role_index', + ) + ); } return array( diff --git a/src/Oro/Bundle/UserBundle/Controller/UserController.php b/src/Oro/Bundle/UserBundle/Controller/UserController.php index 8b70d3b61c7..32ff3ea8889 100644 --- a/src/Oro/Bundle/UserBundle/Controller/UserController.php +++ b/src/Oro/Bundle/UserBundle/Controller/UserController.php @@ -16,6 +16,8 @@ use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\UserBundle\Entity\UserApi; +use Oro\Bundle\OrganizationBundle\Entity\Manager\BusinessUnitManager; + /** * @Acl( * id="oro_user_user", @@ -107,11 +109,20 @@ public function updateAction(User $entity) if ($this->get('oro_user.form.handler.user')->process($entity)) { $this->get('session')->getFlashBag()->add('success', 'User successfully saved'); - return $this->redirect($this->generateUrl('oro_user_index')); + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_user_update', + 'parameters' => array('id' => $entity->getId()), + ), + array( + 'route' => 'oro_user_index', + ) + ); } return array( 'form' => $this->get('oro_user.form.user')->createView(), + 'businessUnits' => $this->getBusinessUnitManager()->getBusinessUnitsTree($entity) ); } @@ -137,4 +148,12 @@ public function indexAction() ? $this->get('oro_grid.renderer')->renderResultsJsonResponse($view) : $this->render('OroUserBundle:User:index.html.twig', array('datagrid' => $view)); } + + /** + * @return BusinessUnitManager + */ + protected function getBusinessUnitManager() + { + return $this->get('oro_organization.business_unit_manager'); + } } diff --git a/src/Oro/Bundle/UserBundle/Datagrid/UserRelationDatagridManager.php b/src/Oro/Bundle/UserBundle/Datagrid/UserRelationDatagridManager.php index cd5d2fa4bfb..1b1d110c067 100644 --- a/src/Oro/Bundle/UserBundle/Datagrid/UserRelationDatagridManager.php +++ b/src/Oro/Bundle/UserBundle/Datagrid/UserRelationDatagridManager.php @@ -24,7 +24,10 @@ abstract protected function createUserRelationColumn(); */ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { - $fieldsCollection->add($this->createUserRelationColumn()); + $relationColumn = $this->createUserRelationColumn(); + if ($relationColumn) { + $fieldsCollection->add($relationColumn); + } $fieldId = new FieldDescription(); $fieldId->setName('id'); diff --git a/src/Oro/Bundle/UserBundle/Entity/Group.php b/src/Oro/Bundle/UserBundle/Entity/Group.php index 1bf6a49a52e..4b93544023f 100644 --- a/src/Oro/Bundle/UserBundle/Entity/Group.php +++ b/src/Oro/Bundle/UserBundle/Entity/Group.php @@ -11,9 +11,17 @@ use BeSimple\SoapBundle\ServiceDefinition\Annotation as Soap; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; +use Oro\Bundle\EntityExtendBundle\Metadata\Annotation\Extend; + /** * @ORM\Entity(repositoryClass="Oro\Bundle\UserBundle\Entity\Repository\GroupRepository") * @ORM\Table(name="oro_access_group") + * @Configurable( + * routeName="oro_user_group_index", + * defaultValues={"entity"={"icon"="group","label"="Group", "plural_label"="Groups"}} + * ) + * @Extend */ class Group { diff --git a/src/Oro/Bundle/UserBundle/Entity/User.php b/src/Oro/Bundle/UserBundle/Entity/User.php index 72831fe6d98..63961bb4509 100644 --- a/src/Oro/Bundle/UserBundle/Entity/User.php +++ b/src/Oro/Bundle/UserBundle/Entity/User.php @@ -24,6 +24,7 @@ use Oro\Bundle\UserBundle\Entity\Status; use Oro\Bundle\UserBundle\Entity\Email; use Oro\Bundle\UserBundle\Entity\EntityUploadedImageInterface; +use Oro\Bundle\OrganizationBundle\Entity\BusinessUnit; use DateTime; @@ -32,6 +33,7 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.ExcessiveClassLength) + * @SuppressWarnings(PHPMD.TooManyFields) * @ORM\Entity(repositoryClass="Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository") * @ORM\Table(name="oro_user") * @ORM\HasLifecycleCallbacks() @@ -284,15 +286,29 @@ class User extends AbstractEntityFlexible implements */ protected $tags; + /** + * @var BusinessUnit[] + * + * @ORM\ManyToMany(targetEntity="\Oro\Bundle\OrganizationBundle\Entity\BusinessUnit", inversedBy="users") + * @ORM\JoinTable(name="oro_user_business_unit", + * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")}, + * inverseJoinColumns={@ORM\JoinColumn(name="business_unit_id", referencedColumnName="id", onDelete="CASCADE")} + * ) + * @Exclude + * @Oro\Versioned("getName") + */ + protected $businessUnits; + public function __construct() { parent::__construct(); - $this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36); - $this->roles = new ArrayCollection(); - $this->groups = new ArrayCollection(); - $this->statuses = new ArrayCollection(); - $this->emails = new ArrayCollection(); + $this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36); + $this->roles = new ArrayCollection(); + $this->groups = new ArrayCollection(); + $this->statuses = new ArrayCollection(); + $this->emails = new ArrayCollection(); + $this->businessUnits = new ArrayCollection(); } /** @@ -1151,4 +1167,51 @@ public function setTags($tags) return $this; } + + /** + * @return ArrayCollection + */ + public function getBusinessUnits() + { + $this->businessUnits = $this->businessUnits ?: new ArrayCollection(); + + return $this->businessUnits; + } + + /** + * @param ArrayCollection $businessUnits + * @return User + */ + public function setBusinessUnits($businessUnits) + { + $this->businessUnits = $businessUnits; + + return $this; + } + + /** + * @param BusinessUnit $businessUnit + * @return User + */ + public function addBusinessUnit(BusinessUnit $businessUnit) + { + if (!$this->getBusinessUnits()->contains($businessUnit)) { + $this->getBusinessUnits()->add($businessUnit); + } + + return $this; + } + + /** + * @param BusinessUnit $businessUnit + * @return User + */ + public function removeBusinessUnit(BusinessUnit $businessUnit) + { + if ($this->getBusinessUnits()->contains($businessUnit)) { + $this->getBusinessUnits()->removeElement($businessUnit); + } + + return $this; + } } diff --git a/src/Oro/Bundle/UserBundle/Form/Handler/UserHandler.php b/src/Oro/Bundle/UserBundle/Form/Handler/UserHandler.php index 8d83132da88..c2a081f2aa7 100644 --- a/src/Oro/Bundle/UserBundle/Form/Handler/UserHandler.php +++ b/src/Oro/Bundle/UserBundle/Form/Handler/UserHandler.php @@ -6,6 +6,8 @@ use Oro\Bundle\TagBundle\Form\Handler\TagHandlerInterface; use Oro\Bundle\UserBundle\Entity\User; +use Oro\Bundle\OrganizationBundle\Entity\Manager\BusinessUnitManager; + class UserHandler extends AbstractUserHandler implements TagHandlerInterface { /** @@ -13,6 +15,38 @@ class UserHandler extends AbstractUserHandler implements TagHandlerInterface */ protected $tagManager; + /** + * @var BusinessUnitManager + */ + protected $businessUnitManager; + + /** + * {@inheritdoc} + */ + public function process(User $user) + { + $this->form->setData($user); + + if (in_array($this->request->getMethod(), array('POST', 'PUT'))) { + $this->form->submit($this->request); + + if ($this->form->isValid()) { + $businessUnits = $this->request->get('businessUnits', array()); + if ($businessUnits) { + $businessUnits = array_keys($businessUnits); + } + if ($this->businessUnitManager) { + $this->businessUnitManager->assignBusinessUnits($user, $businessUnits); + } + $this->onSuccess($user); + + return true; + } + } + + return false; + } + protected function onSuccess(User $user) { $this->manager->updateUser($user); @@ -31,4 +65,12 @@ public function setTagManager(TagManager $tagManager) { $this->tagManager = $tagManager; } + + /** + * @param BusinessUnitManager $businessUnitManager + */ + public function setBusinessUnitManager(BusinessUnitManager $businessUnitManager) + { + $this->businessUnitManager = $businessUnitManager; + } } diff --git a/src/Oro/Bundle/UserBundle/Form/Type/UserMultiSelectType.php b/src/Oro/Bundle/UserBundle/Form/Type/UserMultiSelectType.php new file mode 100644 index 00000000000..75e56c49e2d --- /dev/null +++ b/src/Oro/Bundle/UserBundle/Form/Type/UserMultiSelectType.php @@ -0,0 +1,68 @@ +entityManager = $entityManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + function (FormEvent $event) { + $value = $event->getData(); + if (empty($value)) { + $event->setData(array()); + } + } + ); + $builder->addModelTransformer( + new EntitiesToIdsTransformer($this->entityManager, $options['entity_class']) + ); + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'autocomplete_alias' => 'users', + 'configs' => array( + 'multiple' => true, + 'width' => '400px', + 'placeholder' => 'oro.user.form.choose_user', + 'allowClear' => true, + 'result_template_twig' => 'OroUserBundle:Js:userResult.html.twig', + 'selection_template_twig' => 'OroUserBundle:Js:userSelection.html.twig', + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_user_multiselect'; + } +} diff --git a/src/Oro/Bundle/UserBundle/Form/Type/UserType.php b/src/Oro/Bundle/UserBundle/Form/Type/UserType.php index 296771abe26..355d454f613 100644 --- a/src/Oro/Bundle/UserBundle/Form/Type/UserType.php +++ b/src/Oro/Bundle/UserBundle/Form/Type/UserType.php @@ -2,20 +2,19 @@ namespace Oro\Bundle\UserBundle\Form\Type; -use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; - -use Oro\Bundle\TagBundle\Form\Type\TagSelectType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Security\Core\SecurityContextInterface; use Doctrine\ORM\EntityRepository; + use Oro\Bundle\FlexibleEntityBundle\Form\Type\FlexibleType; use Oro\Bundle\UserBundle\Acl\Manager as AclManager; use Oro\Bundle\UserBundle\Form\EventListener\UserSubscriber; use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\UserBundle\Form\Type\EmailType; +use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; class UserType extends FlexibleType { @@ -54,7 +53,9 @@ public function addEntityFields(FormBuilderInterface $builder) parent::addEntityFields($builder); // user fields - $builder->addEventSubscriber(new UserSubscriber($builder->getFormFactory(), $this->aclManager, $this->security)); + $builder->addEventSubscriber( + new UserSubscriber($builder->getFormFactory(), $this->aclManager, $this->security) + ); $this->setDefaultUserFields($builder); $builder ->add( @@ -92,7 +93,7 @@ public function addEntityFields(FormBuilderInterface $builder) 'type' => 'password', 'required' => true, 'first_options' => array('label' => 'Password'), - 'second_options' => array('label' => 'Password again'), + 'second_options' => array('label' => 'Re-enter password'), ) ) ->add( @@ -118,6 +119,7 @@ public function addEntityFields(FormBuilderInterface $builder) * Add entity fields to form builder * * @param FormBuilderInterface $builder + * @param array $options */ public function addDynamicAttributesFields(FormBuilderInterface $builder, array $options) { diff --git a/src/Oro/Bundle/UserBundle/Resources/config/navigation.yml b/src/Oro/Bundle/UserBundle/Resources/config/navigation.yml index bf7c894c9ca..f3ebd3be54d 100644 --- a/src/Oro/Bundle/UserBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/UserBundle/Resources/config/navigation.yml @@ -7,17 +7,23 @@ oro_menu_config: label: 'Users' route: 'oro_user_index' extras: - routes: ['/^oro_user_(?!role\w+|group\w+)\w+$/'] + routes: ['/^oro_user_(?!role\w+|group\w+|security\w+|reset\w+)\w+$/'] + description: List of all system users + user_create: + label: 'Create User' + route: 'oro_user_create' user_roles: label: 'Roles' route: 'oro_user_role_index' extras: routes: ['oro_user_role_*'] + description: List of all system roles user_groups: label: 'Groups' route: 'oro_user_group_index' extras: routes: ['oro_user_group_*'] + description: List of all system groups shortcut_new_user: label: Create new user route: oro_user_create @@ -30,6 +36,16 @@ oro_menu_config: extras: description: List of all system users isCustomAction: true + shortcut_new_role: + label: Create new role + route: oro_user_role_create + extras: + description: Create new role instance + shortcut_new_group: + label: Create new group + route: oro_user_group_create + extras: + description: Create new group instance tree: application_menu: @@ -44,16 +60,18 @@ oro_menu_config: children: shortcut_new_user: ~ shortcut_list_users: ~ + shortcut_new_role: ~ + shortcut_new_group: ~ oro_titles: - oro_user_view: %%username%% - Users - oro_user_update: %%username%% - Users + oro_user_view: %%username%% + oro_user_update: %%username%% oro_user_create: Create User - oro_user_index: Users + oro_user_index: ~ - oro_user_role_update: %%role%% - Roles + oro_user_role_update: %%role%% oro_user_role_create: Create Role - oro_user_role_index: Roles + oro_user_role_index: ~ oro_user_security_login: Login oro_user_status_list: "User Status - Users" @@ -64,5 +82,5 @@ oro_titles: oro_user_reset_check_email: Password Reset - Check Email oro_user_group_create: Create Group - oro_user_group_update: %%group%% - Groups - oro_user_group_index: Groups + oro_user_group_update: %%group%% + oro_user_group_index: ~ diff --git a/src/Oro/Bundle/UserBundle/Resources/config/services.yml b/src/Oro/Bundle/UserBundle/Resources/config/services.yml index 4697fe76d35..b8d8992da44 100644 --- a/src/Oro/Bundle/UserBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/UserBundle/Resources/config/services.yml @@ -39,6 +39,7 @@ parameters: oro_user.group.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager oro_user.attribute_type.user.class: Oro\Bundle\UserBundle\AttributeType\UserType oro_user.form.type.user_select.class: Oro\Bundle\UserBundle\Form\Type\UserSelectType + oro_user.form.type.user_multiselect.class: Oro\Bundle\UserBundle\Form\Type\UserMultiSelectType oro_user.entity_with_image.subscriber.class: Oro\Bundle\UserBundle\Entity\EventListener\UploadedImageSubscriber oro_user.autocomplete.user.search_handler: Oro\Bundle\UserBundle\Autocomplete\UserSearchHandler @@ -172,6 +173,8 @@ services: arguments: ["@oro_user.form.user", "@request", "@oro_user.manager"] tags: - { name: oro_tag.tag_manager } + calls: + - [setBusinessUnitManager, ["@oro_organization.business_unit_manager"]] oro_user.form.handler.user.api: class: %oro_user.handler.user.class% @@ -223,9 +226,14 @@ services: class: %oro_user.acl.interceptor.class% arguments: ["@service_container"] + oro_user.cache: + parent: oro.cache.abstract + calls: + - [ setNamespace, [ "oro_user.cache" ] ] + oro_user.acl_manager: class: %oro_user.acl.manager.class% - arguments: ["@doctrine.orm.entity_manager", "@oro_user.acl_reader", "@cache", "@security.context", "@oro_user.acl_config_reader"] + arguments: ["@doctrine.orm.entity_manager", "@oro_user.acl_reader", "@oro_user.cache", "@security.context", "@oro_user.acl_config_reader"] oro_user.acl_manager.api: class: %oro_user.acl.manager.api.class% @@ -295,6 +303,13 @@ services: tags: - { name: form.type, alias: oro_user_select } + oro_user.form.type.user_multiselect: + class: %oro_user.form.type.user_multiselect.class% + arguments: + - @doctrine.orm.entity_manager + tags: + - { name: form.type, alias: oro_user_multiselect } + # Flexible attribute oro_user.attribute_type.user: class: %oro_user.attribute_type.user.class% diff --git a/src/Oro/Bundle/UserBundle/Resources/public/js/status.js b/src/Oro/Bundle/UserBundle/Resources/public/js/status.js index a19ba36e834..3c228f4cadc 100644 --- a/src/Oro/Bundle/UserBundle/Resources/public/js/status.js +++ b/src/Oro/Bundle/UserBundle/Resources/public/js/status.js @@ -4,13 +4,13 @@ $(function () { $(".update-status a").click(function () { $.get($(this).attr('href'), function(data) { dialogBlock = $(data).dialog({ - title: "Update status", + title: _.__('Update status'), width: 300, height: 180, modal: false, resizable: false }); - }) + }); return false; }); diff --git a/src/Oro/Bundle/UserBundle/Resources/translations/config.en.yml b/src/Oro/Bundle/UserBundle/Resources/translations/config.en.yml index af716e38739..e9156e8e8d3 100644 --- a/src/Oro/Bundle/UserBundle/Resources/translations/config.en.yml +++ b/src/Oro/Bundle/UserBundle/Resources/translations/config.en.yml @@ -1,6 +1,6 @@ entity: user: - name: Profile + name: User description: Represent user profile role: diff --git a/src/Oro/Bundle/UserBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/UserBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..64b79080c43 --- /dev/null +++ b/src/Oro/Bundle/UserBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1 @@ +"Update status": "Update status" diff --git a/src/Oro/Bundle/UserBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/UserBundle/Resources/views/Form/fields.html.twig new file mode 100644 index 00000000000..21479ddc141 --- /dev/null +++ b/src/Oro/Bundle/UserBundle/Resources/views/Form/fields.html.twig @@ -0,0 +1,5 @@ +{% block oro_combobox_dataconfig_users_multiselect %} + {{ block('oro_combobox_dataconfig_autocomplete') }} + + select2Config.multiple = true; +{% endblock %} diff --git a/src/Oro/Bundle/UserBundle/Resources/views/Group/update.html.twig b/src/Oro/Bundle/UserBundle/Resources/views/Group/update.html.twig index 5848a156f7c..1165931aa09 100644 --- a/src/Oro/Bundle/UserBundle/Resources/views/Group/update.html.twig +++ b/src/Oro/Bundle/UserBundle/Resources/views/Group/update.html.twig @@ -34,7 +34,8 @@ {{ UI.buttonSeparator() }} {% endif %} {{ UI.button({'path' : path('oro_user_group_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} - {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} {% endblock navButtons %} {% block pageHeader %} diff --git a/src/Oro/Bundle/UserBundle/Resources/views/Role/update.html.twig b/src/Oro/Bundle/UserBundle/Resources/views/Role/update.html.twig index 508bf80283f..39b1b85068f 100644 --- a/src/Oro/Bundle/UserBundle/Resources/views/Role/update.html.twig +++ b/src/Oro/Bundle/UserBundle/Resources/views/Role/update.html.twig @@ -48,7 +48,8 @@ {{ UI.buttonSeparator() }} {% endif %} {{ UI.button({'path' : path('oro_user_role_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} - {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} {% endblock navButtons %} {% block pageHeader %} diff --git a/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig b/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig index 28c5b583124..9190a3b3dbd 100644 --- a/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig +++ b/src/Oro/Bundle/UserBundle/Resources/views/User/update.html.twig @@ -7,6 +7,22 @@ {% oro_title_set({params : {"%username%": form.vars.value.fullname(format)|default('N/A') }}) %} {% set entityClass = 'Oro_Bundle_UserBundle_Entity_User' %} {% set formAction = form.vars.value.id ? path('oro_user_update', { id: form.vars.value.id}) : path('oro_user_create') %} +{% block head_script %} + +{% endblock %} {% block navButtons %} {% if form.vars.value.id and resource_granted('oro_user_user_delete') and form.vars.value.id!=app.user.id %} @@ -23,7 +39,8 @@ {{ UI.buttonSeparator() }} {% endif %} {{ UI.button({'path' : path('oro_user_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} - {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} {% endblock navButtons %} {% block pageHeader %} @@ -120,6 +137,27 @@ ) %} {% endif %} + {% macro businessUnitsTree(form, businessUnits) %} +
      + {% include 'OroOrganizationBundle:BusinessUnit:businessUnitsTree.html.twig' with {'businessUnits': businessUnits, 'hasId': form.vars.value.id ? true : false} %} +
      + {% endmacro %} + {% set dataBlocks = dataBlocks|merge( + [{ + 'title': 'Business Units', + 'subblocks': [ + { + 'title': '', + 'useSpan': false, + 'data': [ + _self.businessUnitsTree(form, businessUnits) + ] + } + ] + }] + ) + %} + {% set data = { 'formErrors': form_errors(form)? form_errors(form) : null, diff --git a/src/Oro/Bundle/UserBundle/Tests/Unit/Entity/UserTest.php b/src/Oro/Bundle/UserBundle/Tests/Unit/Entity/UserTest.php index 37ce29e5e95..e847e75f9b9 100644 --- a/src/Oro/Bundle/UserBundle/Tests/Unit/Entity/UserTest.php +++ b/src/Oro/Bundle/UserBundle/Tests/Unit/Entity/UserTest.php @@ -10,6 +10,7 @@ use Oro\Bundle\UserBundle\Entity\Group; use Oro\Bundle\UserBundle\Entity\Status; use Oro\Bundle\UserBundle\Entity\Email; +use Oro\Bundle\OrganizationBundle\Entity\BusinessUnit; /** * @SuppressWarnings(PHPMD.TooManyMethods) @@ -440,4 +441,22 @@ public function testPreUpdate() $user->preUpdate(); $this->assertInstanceOf('\DateTime', $user->getUpdated()); } + + public function testBusinessUnit() + { + $user = new User; + $businessUnit = new BusinessUnit(); + + $user->setBusinessUnits(new ArrayCollection(array($businessUnit))); + + $this->assertContains($businessUnit, $user->getBusinessUnits()); + + $user->removeBusinessUnit($businessUnit); + + $this->assertNotContains($businessUnit, $user->getBusinessUnits()); + + $user->addBusinessUnit($businessUnit); + + $this->assertContains($businessUnit, $user->getBusinessUnits()); + } } diff --git a/src/Oro/Bundle/UserBundle/Tests/Unit/Form/Type/UserMultiSelectTypeTest.php b/src/Oro/Bundle/UserBundle/Tests/Unit/Form/Type/UserMultiSelectTypeTest.php new file mode 100644 index 00000000000..5bfa41946bb --- /dev/null +++ b/src/Oro/Bundle/UserBundle/Tests/Unit/Form/Type/UserMultiSelectTypeTest.php @@ -0,0 +1,96 @@ +em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->type = new UserMultiSelectType($this->em); + } + + public function testBuildView() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + + $this->em->expects($this->once()) + ->method('getClassMetadata') + ->will($this->returnValue($metadata)); + + $metadata->expects($this->once()) + ->method('getSingleIdentifierFieldName') + ->will($this->returnValue('id')); + + $phpUnit = $this; + $builder->expects($this->once()) + ->method('addEventListener') + ->with(FormEvents::PRE_SUBMIT, $this->isInstanceOf('\Closure')) + ->will( + $this->returnCallback( + function ($event, $callback) use ($phpUnit) { + $eventMock = $phpUnit->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + $eventMock->expects($phpUnit->once()) + ->method('getData') + ->will($phpUnit->returnValue('')); + $eventMock->expects($phpUnit->once()) + ->method('setData') + ->with($phpUnit->equalTo(array())); + + $callback($eventMock); + } + ) + ); + + $builder->expects($this->once()) + ->method('addModelTransformer') + ->with($this->isInstanceOf('Oro\Bundle\FormBundle\Form\DataTransformer\EntitiesToIdsTransformer')); + + $this->type->buildForm($builder, array('entity_class' => 'Oro\Bundle\UserBundle\Entity\User')); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + $this->type->setDefaultOptions($resolver); + } + + public function testGetParent() + { + $this->assertEquals('oro_jqueryselect2_hidden', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_user_multiselect', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js index b201b9812f0..731aa8e072f 100644 --- a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js +++ b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js @@ -116,7 +116,14 @@ Oro.widget.DialogView = Backbone.View.extend({ * Handle dialog close */ closeHandler: function() { - this.model.destroy(); + this.model.destroy({ + error: _.bind(function(model, xhr, options) { + // Suppress error if it's 404 response and not debug mode + if (xhr.status != 404 || Oro.debug) { + Oro.BackboneError.Dispatch(model, xhr, options); + } + }, this) + }); this.dialogContent.remove(); this._getActionsElement().remove(); }, From f123a56e642687a951ead7db6d4ec388a2e6e869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gildas=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 1 Aug 2013 17:37:05 +0200 Subject: [PATCH 037/541] Fix bug when trying to convert non object value into string --- .../Entity/Mapping/AbstractEntityFlexibleValue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php index 623a2ee6b05..c5578787e9b 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Mapping/AbstractEntityFlexibleValue.php @@ -526,6 +526,6 @@ public function __toString() return $data->__toString(); } - return $data; + return (string) $data; } } From 6ca7415c2945404b7f1dedbe985b6f5661bc4b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gildas=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 1 Aug 2013 17:37:44 +0200 Subject: [PATCH 038/541] Make media properties nullable --- src/Oro/Bundle/FlexibleEntityBundle/Entity/Media.php | 9 +++++---- .../Bundle/FlexibleEntityBundle/Form/Type/MediaType.php | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Media.php b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Media.php index dea6ec827d5..fc017962812 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Entity/Media.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Entity/Media.php @@ -36,7 +36,7 @@ class Media * * @var string $filename * - * @ORM\Column(name="filename", type="string", length=255, unique=true) + * @ORM\Column(name="filename", type="string", length=255, unique=true, nullable=true) */ protected $filename; @@ -45,7 +45,7 @@ class Media * * @var string $filePath * - * @ORM\Column(name="filepath", type="string", length=255, unique=true) + * @ORM\Column(name="filepath", type="string", length=255, unique=true, nullable=true) */ protected $filePath; @@ -54,15 +54,16 @@ class Media * * @var string $originalFilename * - * @ORM\Column + * @ORM\Column(nullable=true) */ protected $originalFilename; + /** * Mime type * * @var string $mimeType * - * @ORM\Column(name="mimeType", type="string", length=255) + * @ORM\Column(name="mimeType", type="string", length=255, nullable=true) */ protected $mimeType; diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Form/Type/MediaType.php b/src/Oro/Bundle/FlexibleEntityBundle/Form/Type/MediaType.php index f8cc59c4806..7b1263aec7c 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Form/Type/MediaType.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Form/Type/MediaType.php @@ -20,8 +20,7 @@ class MediaType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('id', 'hidden') - ->add('file', 'file', array('required' => false,)) + ->add('file', 'file', array('required' => false)) ->add( 'removed', 'checkbox', From 98d4edae4c94b245232b9e8159d499bdc8ba824e Mon Sep 17 00:00:00 2001 From: Michael Banin Date: Thu, 1 Aug 2013 18:42:02 +0300 Subject: [PATCH 039/541] BAP-1173 Test fixes --- .../Tests/Selenium/Tags/TagsAcl.php | 199 ++++++++++-------- 1 file changed, 115 insertions(+), 84 deletions(-) diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php index d9548f6fd10..aa224ae76f7 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php @@ -116,104 +116,135 @@ public function testTagAcl($aclcase, $username, $role, $tagname) ->submit(); switch ($aclcase) { case 'delete': - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('Delete tags') - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openTags() - ->checkContextMenu($tagname, 'Delete'); + $this->deleteAcl($login, $role, $username, $tagname); break; case 'update': - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('Update tag') - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openTags() - ->checkContextMenu($tagname, 'Update'); + $this->updateAcl($login, $role, $username, $tagname); break; case 'create': - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('Create tag') - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openTags() - ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]"); + $this->createAcl($login, $role, $username); break; case 'view list': - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('View list of tags') - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openTags() - ->assertTitle('403 - Forbidden'); + $this->viewListAcl($login, $role, $username); break; case 'unassign global': - $username = 'user' . mt_rand(); - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('Tag unassign global') - ->save() - ->openUsers() - ->add() - ->setUsername($username) - ->enable() - ->setFirstpassword('123123q') - ->setSecondpassword('123123q') - ->setFirstname('First_'.$username) - ->setLastname('Last_'.$username) - ->setEmail($username.'@mail.com') - ->setRoles(array('Manager')) - ->setTag($tagname) - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openUsers() - ->filterBy('Username', $username) - ->open(array($username)) - ->edit() - ->assertElementNotpresent - ("//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']"); + $this->unassignGlobalAcl($login, $role, $tagname); break; case 'assign/unassign': - $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) - ->selectAcl('Tag assign/unassign') - ->save() - ->logout() - ->setUsername($username) - ->setPassword('123123q') - ->submit() - ->openAccounts() - ->add(false) - ->assertElementPresent("//div[@class='select2-container select2-container-multi select2-container-disabled']"); + $this->assignAcl($login, $role, $username); break; } } + public function deleteAcl($login, $role, $username, $tagname) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Delete tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Delete'); + } + + public function updateAcl($login, $role, $username, $tagname) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Update tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Update'); + } + + public function createAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Create tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]"); + } + + public function viewListAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('View list of tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertTitle('403 - Forbidden'); + } + + public function unassignGlobalAcl($login, $role, $tagname) + { + $username = 'user' . mt_rand(); + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Tag unassign global') + ->save() + ->openUsers() + ->add() + ->setUsername($username) + ->enable() + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array($role)) + ->setTag($tagname) + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openUsers() + ->filterBy('Username', $username) + ->open(array($username)) + ->edit() + ->assertElementNotPresent( + "//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']" + ); + } + + public function assignAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Tag assign/unassign') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openAccounts() + ->add(false) + ->assertElementPresent("//div[@class='select2-container select2-container-multi select2-container-disabled']"); + } + /** * Data provider for Tags ACL test * From 8af9da034902efc5c79ce5043e0826081c815115 Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Thu, 1 Aug 2013 18:10:29 +0200 Subject: [PATCH 040/541] PIM-873 : merge flexible query builder --- .../Doctrine/ORM/FlexibleQueryBuilder.php | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php index 92fef1eee51..8f4e17382d0 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Doctrine/ORM/FlexibleQueryBuilder.php @@ -1,4 +1,5 @@ expr()->eq($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->eq($field, $this->qb->expr()->literal($value))->__toString(); break; case '<': - $condition = $this->expr()->lt($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->lt($field, $this->qb->expr()->literal($value))->__toString(); break; case '<=': - $condition = $this->expr()->lte($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->lte($field, $this->qb->expr()->literal($value))->__toString(); break; case '>': - $condition = $this->expr()->gt($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->gt($field, $this->qb->expr()->literal($value))->__toString(); break; case '>=': - $condition = $this->expr()->gte($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->gte($field, $this->qb->expr()->literal($value))->__toString(); break; case 'LIKE': - $condition = $this->expr()->like($field, $this->expr()->literal($value))->__toString(); + $condition = $this->qb->expr()->like($field, $this->qb->expr()->literal($value))->__toString(); break; case 'NOT LIKE': - $condition = sprintf('%s NOT LIKE %s', $field, $this->expr()->literal($value)); + $condition = sprintf('%s NOT LIKE %s', $field, $this->qb->expr()->literal($value)); break; case 'NULL': - $condition = $this->expr()->isNull($field); + $condition = $this->qb->expr()->isNull($field); break; case 'NOT NULL': - $condition = $this->expr()->isNotNull($field); + $condition = $this->qb->expr()->isNotNull($field); break; case 'IN': - $condition = $this->expr()->in($field, $value)->__toString(); + $condition = $this->qb->expr()->in($field, $value)->__toString(); break; case 'NOT IN': - $condition = $this->expr()->notIn($field, $value)->__toString(); + $condition = $this->qb->expr()->notIn($field, $value)->__toString(); break; default: throw new FlexibleQueryException('operator '.$operator.' is not supported'); @@ -282,13 +283,26 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val // inner join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->innerJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->innerJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then join to option with filter on option id $joinAliasOpt = 'filterO'.$attribute->getCode().$this->aliasCounter; $backendField = sprintf('%s.%s', $joinAliasOpt, 'id'); $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); - $this->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + $this->qb->innerJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + + } else if ($attribute->getBackendType() == AbstractAttributeType::BACKEND_TYPE_ENTITY) { + + // inner join to value + $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); + $rootAlias = $this->qb->getRootAliases(); + $this->qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + + // then join to linked entity with filter on id + $joinAliasOpt = 'filterentity'.$attribute->getCode().$this->aliasCounter; + $backendField = sprintf('%s.id', $joinAliasEntity); + $condition = $this->prepareCriteriaCondition($backendField, $operator, $value); + $this->qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); } else { @@ -296,7 +310,7 @@ public function addAttributeFilter(AbstractAttribute $attribute, $operator, $val $backendField = sprintf('%s.%s', $joinAlias, $attribute->getBackendType()); $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); $condition .= ' AND '.$this->prepareCriteriaCondition($backendField, $operator, $value); - $this->innerJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->innerJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); } return $this; @@ -318,25 +332,26 @@ public function addAttributeOrderBy(AbstractAttribute $attribute, $direction) // join to value $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->leftJoin($this->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->leftJoin($this->qb->getRootAlias().'.' . $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); // then to option and option value to sort on $joinAliasOpt = $aliasPrefix.'O'.$attribute->getCode().$this->aliasCounter; $condition = $joinAliasOpt.".attribute = ".$attribute->getId(); - $this->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); + $this->qb->leftJoin($joinAlias.'.'.$attribute->getBackendType(), $joinAliasOpt, 'WITH', $condition); $joinAliasOptVal = $aliasPrefix.'OV'.$attribute->getCode().$this->aliasCounter; - $condition = $joinAliasOptVal.'.locale = '.$this->expr()->literal($this->getLocale()); - $this->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); + $condition = $joinAliasOptVal.'.locale = '.$this->qb->expr()->literal($this->getLocale()); + $this->qb->leftJoin($joinAliasOpt.'.optionValues', $joinAliasOptVal, 'WITH', $condition); - $this->addOrderBy($joinAliasOptVal.'.value', $direction); + $this->qb->addOrderBy($joinAliasOpt.'.defaultValue', $direction); + $this->qb->addOrderBy($joinAliasOptVal.'.value', $direction); } else { // join to value and sort on $condition = $this->prepareAttributeJoinCondition($attribute, $joinAlias); - $this->leftJoin($this->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - $this->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); + $this->qb->leftJoin($this->qb->getRootAlias().'.'.$attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); + $this->qb->addOrderBy($joinAlias.'.'.$attribute->getBackendType(), $direction); } } -} +} \ No newline at end of file From b2a9c073ebe3d426c06939a0fed0bf46b2ae1244 Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Thu, 1 Aug 2013 19:00:17 +0200 Subject: [PATCH 041/541] PIM-873 : fix media test --- .../FlexibleEntityBundle/Tests/Unit/Form/Type/MediaTypeTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Form/Type/MediaTypeTest.php b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Form/Type/MediaTypeTest.php index 3f4e1ebd2d8..484b5decde1 100644 --- a/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Form/Type/MediaTypeTest.php +++ b/src/Oro/Bundle/FlexibleEntityBundle/Tests/Unit/Form/Type/MediaTypeTest.php @@ -27,7 +27,6 @@ protected function setUp() */ public function testFormCreate() { - $this->assertField('id', 'hidden'); $this->assertField('file', 'file'); $this->assertField('removed', 'checkbox'); From 2d3cd39f09e9c058188bc8010ec41f344d881f75 Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Thu, 1 Aug 2013 17:08:35 +0000 Subject: [PATCH 042/541] BAP-1279: Ability to disable paginator, "All" option to # of records - add 'All' option to grid paginator - disable/enable paginator from grid manager --- .../Datagrid/EmailTemplateDatagridManager.php | 15 +++++++++++++ .../Bundle/GridBundle/Datagrid/Datagrid.php | 22 +++++++++++++++++++ .../GridBundle/Datagrid/DatagridInterface.php | 11 ++++++++++ .../GridBundle/Datagrid/DatagridManager.php | 13 +++++++++++ .../Datagrid/DatagridManagerInterface.php | 7 ++++++ .../public/js/app/datagrid/pagesize.js | 20 +++++++++++++---- .../public/js/app/pageablecollection.js | 8 +++---- .../views/Include/javascript.html.twig | 5 +---- 8 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php index 42adbc6f474..8fe5b5f8bf2 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php @@ -207,4 +207,19 @@ protected function getRowActions() return array($clickAction, $updateAction, $cloneAction, $deleteAction); } + + /** + * Return toolbar options + * + * @return array + */ + public function getToolbarOptions() + { + return array( + 'enable' => true, + 'pageSize' => array( + 'items' => array(10, 20, 50, 100, array('size' => 0, 'label' => 'All')), + ), + ); + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php b/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php index dcf851649d1..6cc7641df0b 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php @@ -111,6 +111,11 @@ class Datagrid implements DatagridInterface */ protected $rowActions; + /** + * @var + */ + protected $toolbarOptions; + /** * @param ProxyQueryInterface $query * @param FieldDescriptionCollection $columns @@ -473,4 +478,21 @@ public function createView() { return new DatagridView($this); } + + /** + * @return array + */ + public function getToolbarOptions() + { + return $this->toolbarOptions; + } + + /** + * @param $options + * @return $this + */ + public function setToolbarOptions($options) + { + $this->toolbarOptions = $options; + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php index d0d60ecb0d0..dfcc97e1b15 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php @@ -74,4 +74,15 @@ public function getParameters(); * @return array */ public function getProperties(); + + /** + * @return array + */ + public function getToolBarOptions(); + + /** + * @param $options + * @return $this + */ + public function setToolbarOptions($options); } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php index 60c80471d13..4a6c1fa1a9a 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php @@ -222,6 +222,9 @@ public function getDatagrid() $this->datagridBuilder->addRowAction($datagrid, $actionParameters); } + // add toolbar options + $datagrid->setToolbarOptions($this->getToolbarOptions()); + return $datagrid; } @@ -421,4 +424,14 @@ protected function translate($id, array $parameters = array(), $domain = null) return $this->translator->trans($id, $parameters, $domain); } + + /** + * Define grid toolbar options as assoc array + * + * @return array + */ + public function getToolBarOptions() + { + return array(); + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php index 98a2e17efde..3e56c379115 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php @@ -88,4 +88,11 @@ public function setRouteGenerator(RouteGeneratorInterface $routeGenerator); * @return void */ public function setParameters(ParametersInterface $parameters); + + /** + * Define grid toolbar options as assoc array + * + * @return array + */ + public function getToolbarOptions(); } diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js index 8d051b3718c..a7f9832809b 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js @@ -13,11 +13,12 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ '' + '
      ' + '' + '' + '
      ' @@ -88,7 +89,7 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ */ onChangePageSize: function (e) { e.preventDefault(); - var pageSize = parseInt($(e.target).text()); + var pageSize = parseInt($(e.target).data('size')); if (pageSize !== this.collection.state.pageSize) { this.collection.state.pageSize = pageSize; this.collection.fetch(); @@ -98,10 +99,21 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ render: function() { this.$el.empty(); + var self = this; + var currentSizeLabel = _.filter(this.items, function(item) { + if (item.label == undefined) { + return self.collection.state.pageSize == item; + } else { + return self.collection.state.pageSize == item.size; + } + }); + currentSizeLabel = currentSizeLabel[0].label == undefined ? currentSizeLabel[0] : currentSizeLabel[0].label; + this.$el.append($(this.template({ disabled: !this.enabled || !this.collection.state.totalRecords, collectionState: this.collection.state, - items: this.items + items: this.items, + currentSizeLabel: currentSizeLabel }))); return this; diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/pageablecollection.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/pageablecollection.js index 2dda9862de5..b3e041b58a6 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/pageablecollection.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/pageablecollection.js @@ -223,7 +223,7 @@ Oro.PageableCollection = Backbone.PageableCollection.extend({ var mode = this.mode; var links = this.links; var totalRecords = state.totalRecords; - var pageSize = state.pageSize; + var pageSize = state.pageSize == 'All' ? 0 : state.pageSize; var currentPage = state.currentPage; var firstPage = state.firstPage; var totalPages = state.totalPages; @@ -236,11 +236,11 @@ Oro.PageableCollection = Backbone.PageableCollection.extend({ state.currentPage = currentPage = this.finiteInt(currentPage, "currentPage"); state.firstPage = firstPage = this.finiteInt(firstPage, "firstPage"); - if (pageSize < 1) { - throw new RangeError("`pageSize` must be >= 1"); + if (pageSize < 0) { + throw new RangeError("`pageSize` must be >= 0"); } - state.totalPages = totalPages = state.totalPages = Math.ceil(totalRecords / pageSize); + state.totalPages = pageSize == 0 ? 1 : totalPages = state.totalPages = Math.ceil(totalRecords / pageSize); if (firstPage < 0 || firstPage > 1) { throw new RangeError("`firstPage` must be 0 or 1"); diff --git a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig index 68c624b7d09..e727bfebbb2 100644 --- a/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig +++ b/src/Oro/Bundle/GridBundle/Resources/views/Include/javascript.html.twig @@ -95,10 +95,7 @@ }, entityHint : {{ datagrid.entityHint|capitalize|json_encode|raw }}, noDataHint : {{ 'oro_grid.no_data_hint %entityHint%'|trans({'%entityHint%': entityHint}, 'OroGridBundle')|json_encode|raw }}, - toolbarOptions: { - enable: false, - pageSize: { items: [10, 20, 50, 100, 'All']} - } + toolbarOptions: {{ datagrid.toolbarOptions|json_encode|raw }}, }, collection: { inputName: {{ datagrid.name|json_encode|raw }}, From 838de1e87d17347b00080caab18cc76dff40c589 Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Thu, 1 Aug 2013 19:52:49 +0200 Subject: [PATCH 043/541] BAP-1232: Wrong behaviour on adding new tags to entity --- .../Resources/views/Form/fields.html.twig | 7 ++ .../Bundle/TagBundle/Entity/TagManager.php | 73 ++++++++++- .../TagBundle/Form/DataMapper/TagMapper.php | 100 +++++++++++++++ .../Form/EventSubscriber/TagSubscriber.php | 116 ++++++++++++++++++ .../TagTransformer.php} | 20 +-- .../Form/Type/TagAutocompleteType.php | 1 - .../TagBundle/Form/Type/TagSelectType.php | 45 ++++--- .../TagBundle/Resources/config/assets.yml | 2 +- .../TagBundle/Resources/config/services.yml | 24 +++- .../TagBundle/Resources/public/css/tag.css | 3 +- .../Resources/public/js/collections/tag.js | 14 ++- .../Resources/public/js/views/select2.js | 86 ------------- .../Resources/public/js/views/tag.js | 13 +- .../Resources/public/js/views/tag_update.js | 97 +++++++++++++++ .../Resources/views/Form/fields.html.twig | 68 +++++----- .../Bundle/TagBundle/Twig/TagExtension.php | 69 +---------- 16 files changed, 509 insertions(+), 229 deletions(-) create mode 100644 src/Oro/Bundle/TagBundle/Form/DataMapper/TagMapper.php create mode 100644 src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php rename src/Oro/Bundle/TagBundle/Form/{TagsTransformer.php => Transformer/TagTransformer.php} (89%) delete mode 100644 src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js create mode 100644 src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js diff --git a/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig index 3ba49b8c314..a0974611cad 100644 --- a/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig @@ -61,6 +61,13 @@ {{ block('genemu_jqueryselect2_row') }} {% endblock %} +{% block oro_jqueryselect2_hidden_widget %} +
      + {{ form_widget(form) }} + {{ form_javascript(form) }} +
      +{% endblock %} + {% block genemu_jqueryselect2_row %} {{ form_row(form) }} {{ form_javascript(form) }} diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index ec16f363940..5ed08da7bf2 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -9,6 +9,8 @@ use Oro\Bundle\SearchBundle\Engine\ObjectMapper; use Oro\Bundle\UserBundle\Acl\Manager; +use Oro\Bundle\UserBundle\Entity\User; +use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\Security\Core\SecurityContextInterface; @@ -45,13 +47,19 @@ class TagManager */ protected $aclManager; + /** + * @var Router + */ + protected $router; + public function __construct( EntityManager $em, $tagClass, $taggingClass, ObjectMapper $mapper, SecurityContextInterface $securityContext, - Manager $aclManager + Manager $aclManager, + Router $router ) { $this->em = $em; @@ -60,6 +68,7 @@ public function __construct( $this->mapper = $mapper; $this->securityContext = $securityContext; $this->aclManager = $aclManager; + $this->router = $router; } /** @@ -100,6 +109,25 @@ public function removeTag(Tag $tag, Taggable $resource) return $resource->getTags()->removeElement($tag); } + /** + * @param array $ids + */ + public function loadTags(array $ids) + { + $builder = $this->em->createQueryBuilder(); + + $tags = $builder + ->select('t') + ->from($this->tagClass, 't') + + ->where($builder->expr()->in('t.id', $ids)) + + ->getQuery() + ->getResult(); + + return $tags; + } + /** * Loads or creates multiples tags from a list of tag names * @@ -145,6 +173,49 @@ public function loadOrCreateTags(array $names) return $tags; } + /** + * Prepare array + * + * @param Taggable $entity + * @return array + */ + public function getPreparedArray(Taggable $entity) + { + $this->loadTagging($entity); + $result = array(); + + /** @var Tag $tag */ + foreach ($entity->getTags() as $tag) { + $entry = array( + 'name' => $tag->getName(), + 'id' => $tag->getId(), + 'url' => $this->router->generate('oro_tag_search', array('id' => $tag->getId())) + ); + + $taggingCollection = $tag->getTagging()->filter( + function (Tagging $tagging) use ($entity) { + // only use tagging entities that related to current entity + return $tagging->getEntityName() == get_class($entity) + && $tagging->getRecordId() == $entity->getTaggableId(); + } + ); + /** @var Tagging $tagging */ + foreach ($taggingCollection as $tagging) { + if ($this->getUser()->getId() == $tagging->getCreatedBy()->getId()) { + $entry['owner'] = true; + } + } + + if (!$this->aclManager->isResourceGranted('oro_tag_unassign_global') && !isset($entry['owner'])) { + $entry['locked'] = true; + } + + $result[] = $entry; + } + + return $result; + } + /** * Saves tags for the given taggable resource * diff --git a/src/Oro/Bundle/TagBundle/Form/DataMapper/TagMapper.php b/src/Oro/Bundle/TagBundle/Form/DataMapper/TagMapper.php new file mode 100644 index 00000000000..203a390cd2e --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/DataMapper/TagMapper.php @@ -0,0 +1,100 @@ +manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function mapDataToForms($data, $forms) + { + if (null === $data) { + return; + } + + if (!is_array($data) && !is_object($data)) { + throw new UnexpectedTypeException($data, 'object, array or empty'); + } + + $tags = $this->manager->prepareArray($data); + $ownTags = array_filter( + $tags, + function ($item) { + return isset($item['owner']) && $item['owner']; + } + ); + + foreach ($forms as $field) { + $name = $field->getConfig()->getName(); + + switch($name) { + case 'all': + $field->setData($tags); + break; + case 'own': + $field->setData($ownTags); + break; + } + } + } + + /** + * {@inheritdoc} + */ + public function mapFormsToData($forms, &$data) + { + $a =1; + + if (null === $data) { + return; + } + + if (!is_array($data) && !is_object($data)) { + throw new UnexpectedTypeException($data, 'object, array or empty'); + } + +// $newData = new ArrayCollection(); +// +// foreach ($forms as $translationsFieldsForm) { +// $locale = $translationsFieldsForm->getConfig()->getName(); +// +// foreach ($translationsFieldsForm->getData() as $field => $content) { +// $existingTranslation = $data ? $data->filter(function ($object) use ($locale, $field) { +// return ($object && ($object->getLocale() === $locale) && ($object->getField() === $field)); +// })->first() : null; +// +// if ($existingTranslation) { +// $existingTranslation->setContent($content); +// $newData->add($existingTranslation); +// +// } else { +// $translation = new $this->translationClass(); +// $translation->setLocale($locale); +// $translation->setField($field); +// $translation->setContent($content); +// $newData->add($translation); +// } +// } +// } +// +// $data = $newData; + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php new file mode 100644 index 00000000000..e4cbd98f930 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php @@ -0,0 +1,116 @@ +manager = $manager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => 'preSet', + FormEvents::PRE_SUBMIT => 'preSubmit' + ); + } + + /** + * {@inheritdoc} + */ + public function preSet(FormEvent $event) + { + $entity = $event->getForm()->getParent()->getData(); + + if (!$entity instanceof Taggable || $entity->getTaggableId() == null) { + // do nothing if new entity or some error + return; + } + + $tags = $this->manager->getPreparedArray($entity); + $ownTags = array_filter( + $tags, + function ($item) { + return isset($item['owner']) && $item['owner']; + } + ); + + $event->setData( + array( + 'autocomplete' => null, + 'all' => json_encode($tags), + 'owner' => json_encode($ownTags) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function preSubmit(FormEvent $event) + { + $values = $event->getData(); + $entities = array( + 'all' => array(), + 'owner' => array() + ); + + if (!isset($values['all'], $values['owner'])) { + return $values; + } + + foreach (array_keys($entities) as $type) { + if (!is_array($values[$type])) { + $values[$type] = json_decode($values[$type]); + } + + $newValues[$type] = array_filter( + $values[$type], + function ($item) { + return !intval($item->id) && !empty($item->name); + } + ); + + $newValues[$type] = array_map( + function ($item) { + return $item->name; + }, + $newValues[$type] + ); + + $values[$type] = array_map( + function ($item) { + return $item->id; + }, + $values[$type] + ); + + if ($values[$type]) { + $entities[$type] = $this->manager->loadTags($values[$type]); + } + + if ($newValues[$type]) { + $entities[$type] = array_merge($entities[$type], $this->manager->loadOrCreateTags($newValues[$type])); + } + } + + $event->setData($entities); + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/TagsTransformer.php b/src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php similarity index 89% rename from src/Oro/Bundle/TagBundle/Form/TagsTransformer.php rename to src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php index 05aeb60e5ce..7c2a51d5e1d 100644 --- a/src/Oro/Bundle/TagBundle/Form/TagsTransformer.php +++ b/src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php @@ -1,11 +1,12 @@ tagManager = $manager; + } + /** * {@inheritdoc} */ public function transform($value) { + $tags = array(); return $value; } @@ -30,6 +40,7 @@ public function transform($value) */ public function reverseTransform($values) { + $a =1; $entities = array( 'all' => array(), 'owner' => array() @@ -98,9 +109,4 @@ protected function loadEntitiesByIds(array $ids) return $qb->getQuery()->execute(); } - - public function setTagManager(TagManager $tagManager) - { - $this->tagManager = $tagManager; - } } diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php index 924d56e8673..a6a2898047e 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php @@ -21,7 +21,6 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'multiple' => true ), 'autocomplete_alias' => 'tags', - 'inherit_data' => true ) ); diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php index f9b22f5ffb0..f6a076a531b 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php @@ -1,32 +1,33 @@ om = $om; - $this->tagManager = $tagManager; + $this->transformer = $transformer; + $this->subscriber = $subscriber; + + $this->mapper = $mapper; } /** @@ -36,9 +37,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( array( - 'fields' => array(), // ? - 'form' => array(), // ? - 'inherit_data' => true, + 'required' => false, ) ); } @@ -48,10 +47,24 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) */ public function buildForm(FormBuilderInterface $builder, array $options) { + $builder->addEventSubscriber($this->subscriber); +// $builder->addViewTransformer($this->transformer); +// $builder->setDataMapper($this->mapper); + $builder->add( 'autocomplete', 'oro_tag_autocomplete' ); + + $builder->add( + 'all', + 'hidden' + ); + + $builder->add( + 'owner', + 'hidden' + ); } /** diff --git a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml index 9f352b28ea9..601a3786e7a 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml @@ -6,5 +6,5 @@ js: 'tags': - '@OroTagBundle/Resources/public/js/models/tag.js' - '@OroTagBundle/Resources/public/js/views/tag.js' + - '@OroTagBundle/Resources/public/js/views/tag_update.js' - '@OroTagBundle/Resources/public/js/collections/tag.js' - - '@OroTagBundle/Resources/public/js/views/select2.js' diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index 05245887966..85e86e8792b 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -14,6 +14,9 @@ parameters: oro_tag.autocomplete.tag.search_handler.class: Oro\Bundle\FormBundle\Autocomplete\SearchHandler oro_tag.form.type.tag_select.class: Oro\Bundle\TagBundle\Form\Type\TagSelectType oro_tag.form.type.tag_autocomplete.class: Oro\Bundle\TagBundle\Form\Type\TagAutocompleteType + oro_tag.form.subscriber.tag_select.class: Oro\Bundle\TagBundle\Form\EventSubscriber\TagSubscriber + oro_tag.form.transformer.tag_select.class: Oro\Bundle\TagBundle\Form\Transformer\TagTransformer + oro_tag.form.mapper.tag_select.class: Oro\Bundle\TagBundle\Form\DataMapper\TagMapper oro_tag.provider.search_provider.class: Oro\Bundle\TagBundle\Provider\SearchProvider @@ -29,6 +32,7 @@ services: - @oro_search.mapper - @security.context - @oro_user.acl_manager + - @router oro_tag.docrine.event.listener: class: %oro_tag.tag_listener.class% @@ -115,18 +119,28 @@ services: oro_tag.form.type.tag_select: class: %oro_tag.form.type.tag_select.class% arguments: - - @doctrine.orm.entity_manager - - @oro_tag.tag.manager + - @oro_tag.form.transformer.tag_select + - @oro_tag.form.subscriber.tag_select + - @oro_tag.form.mapper.tag_select tags: - { name: form.type, alias: oro_tag_select } + oro_tag.form.transformer.tag_select: + class: %oro_tag.form.transformer.tag_select.class% + arguments: [@oro_tag.tag.manager] + + oro_tag.form.mapper.tag_select: + class: %oro_tag.form.mapper.tag_select.class% + arguments: [@oro_tag.tag.manager] + + oro_tag.form.subscriber.tag_select: + class: %oro_tag.form.subscriber.tag_select.class% + arguments: [@oro_tag.tag.manager] + # Twig extension oro_tag.twig.tag.extension: class: %oro_tag.twig.tag.extension.class% arguments: - @oro_tag.tag.manager - - @router - - @security.context - - @oro_user.acl_manager tags: - { name: twig.extension } diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css b/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css index f92f77723f3..102b41ebf0f 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css @@ -1,7 +1,8 @@ +#tags-overlay #tag-list, .tag-view #tag-list { margin-bottom: 0; } -.tag-view #tag-sort-actions { +#tag-sort-actions { margin-top: 5px; } #tag-sort-actions a.active:hover, diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js index f5f6378e713..0b2f02cf76e 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js @@ -22,12 +22,14 @@ Oro.Tags.TagCollection = Backbone.Collection.extend({ return new Oro.Tags.TagCollection(filtered); }, - toArray: function() { - var tagArray = []; - _.each(this.models, function(tag) { - tagArray.push(tag.attributes); - }); + /** + * Used for adding item on tag_update view + * @param {Object} value + */ + addItem: function(value) { + var tag = new this.model({id: value.id, name: value.name}); - return tagArray; + this.add(tag); + this.trigger('addItem'); } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js deleted file mode 100644 index 3dc277bb831..00000000000 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js +++ /dev/null @@ -1,86 +0,0 @@ -Oro = Oro || {}; -Oro.Tags = Oro.Tags || {}; - -Oro.Tags.Select2View = Oro.Tags.TagView.extend({ - options: { - tagInputId: null, - tags: null - }, - - /** - * Constructor - */ - initialize: function() { - this.collection = new Oro.Tags.TagCollection(); - this.ownCollection = new Oro.Tags.TagCollection(); - - this.listenTo(this.getCollection(), 'reset', this.render); - this.listenTo(this.getCollection('owner'), 'reset', this.render); - this.listenTo(this, 'filter', this.render); - - $('#tag-sort-actions a').click(_.bind(this.filter, this)); - - self = this; - $(this.options.tagInputId).on("change", this.updateHiddenInputs); - - if (this.options.tags != null) { - this.getCollection().reset(this.options.tags); - this.getCollection('owner').reset(this.getCollection().getFilteredCollection('owner').models); - } - }, - - getCollection: function(type) { - if (type == undefined || type == 'all') { - return this.collection; - } else if (type == 'owner') { - return this.ownCollection; - } else { - return {}; - } - }, - - updateHiddenInputs: function(event) { - var owner = self.options.filter == undefined ? 'all' : self.options.filter; - - if (event && event.added) { - event.added.owner = true; - owner = 'owner'; - self.getCollection('owner').add(event.added); - } - else if (event && event.removed) { - self.getCollection(owner).remove(event.removed); - } - - $(self.options.tagInputId + '_owner').val(self.getCollection('owner').pluck('id')); - $(self.options.tagInputId + '_all').val(self.getCollection().pluck('id')); - - $(self.options.tagInputId).select2('data', self.getCollection(owner).toArray()); - }, - - /** - * Render widget - * - * @returns {} - */ - render: function() { - this.updateHiddenInputs(); - - var tagCollection = new Oro.Tags.TagCollection(); - tagCollection.add(this.getCollection().models); - tagCollection.add(this.getCollection('owner').models); - - $('.select2-search-choice div').click(function(){ - var tagName = $(this).html(); - var tag = tagCollection.toArray().filter(function(item){ return item.name == tagName }) - var url = tag[0].url; - - if (Oro.hashNavigationEnabled()) { - Oro.hashNavigationInstance.setLocation(url); - } else { - window.location = url; - } - }); - - return this; - } -}); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js index 9b6c1446b6d..48ece7bb18d 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js @@ -7,15 +7,22 @@ Oro.Tags.TagView = Backbone.View.extend({ }, /** @property */ - template:_.template( + template: _.template( '' ), diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js new file mode 100644 index 00000000000..1667b55ab28 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js @@ -0,0 +1,97 @@ +Oro = Oro || {}; +Oro.Tags = Oro.Tags || {}; + +Oro.Tags.TagsUpdateView = Oro.Tags.TagView.extend({ + /** @property */ + tagsOverlayTemplate: _.template( + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + ), + + /** @property {Object} */ + options: { + filter: null, + tagsOverlayId: '#tags-overlay', + autocompleteFieldId: null, + fieldId: null, + ownFieldId: null + }, + + /** + * Initialize widget + * + * @param {Object} options + * @param {Backbone.Collection} options.tags + * @param {String} options.autocompleteFieldId DomElement ID of autocomplete widget + * @param {String} options.fieldId DomElement ID of hidden field with all tags + * @param {String} options.ownFieldId DomElement ID of hidden field with own tags + * @throws {TypeError} If mandatory options are undefined + */ + initialize: function(options) { + options = options || {}; + + if (!options.autocompleteFieldId) { + throw new TypeError("'autocompleteFieldId' is required") + } + + if (!options.fieldId) { + throw new TypeError("'fieldId' is required") + } + + if (!options.ownFieldId) { + throw new TypeError("'ownFieldId' is required") + } + + Oro.Tags.TagView.prototype.initialize.apply(this, arguments); + + this._prepareCollections(); + this._renderOverlay() + this.listenTo(this.getCollection(), 'addItem', this.render); + + $(this.options.autocompleteFieldId).on('change', _.bind(this._addItem, this)); + }, + + /** + * Add item from autocomplete to internal collection + * + * @param {Object} e select2.change event object + * @private + */ + _addItem: function(e) { + e.preventDefault(); + var tag = e.added; + + if (!_.isUndefined(tag)) { + this.collection.addItem(tag); + } + + // clear autocomplete + $(e.target).select2('val', ''); + }, + + /** + * Render overlay block + * + * @returns {*} + * @private + */ + _renderOverlay: function() { + $(this.options.tagsOverlayId).append(this.tagsOverlayTemplate()); + + return this; + }, + + /** + * Fill data to collections from hidden inputs + * + * @returns {*} + * @private + */ + _prepareCollections: function() { +// var ownTags = + return this; + } +}); diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index 7fc4dd2ef04..a3eae508ae3 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -1,50 +1,48 @@ {% block oro_tag_select_row %} - {% import 'OroTagBundle::macros.html.twig' as _tag %} +
      + {{ form_label(form, '' , { label_attr: label_attr|merge({ class: 'control-label' })}) }} - {% if resource_granted('oro_tag_assign_unassign') %} -
      -
      -
      - {{ _tag.tagSortActions() }} + {% import 'OroTagBundle::macros.html.twig' as _tag %} + + {% if resource_granted('oro_tag_assign_unassign') %} +
      +
      +
      + {{ _tag.tagSortActions() }} +
      -
      - {{ form_row(form.autocomplete) }} - {% endif %} + {{ form_widget(form.autocomplete) }} + {% endif %} - {# show all tags #} - {#{{ form_row(form.autocomplete) }}#} + {{ form_rest(form) }} +
      +
      +
      - {# show field with mine tags#} - {#{% if resource_granted('oro_tag_assign_unassign') %}#} - {#{{ form_row(form.autocomplete) }}#} - {#{% endif %}#} + {# show all tags #} + {{ form_row(form.autocomplete) }} - {##} - + +
      {% endblock %} - {% block oro_combobox_dataconfig_multi_autocomplete %} {{ block('oro_combobox_dataconfig_autocomplete') }} @@ -54,7 +52,7 @@ return this.name.localeCompare(term) === 0; }).length === 0 ) { - {% if resource_granted('oro_tag_create') %} + {% if not resource_granted('oro_tag_create') %} return null; {% else %} return { diff --git a/src/Oro/Bundle/TagBundle/Twig/TagExtension.php b/src/Oro/Bundle/TagBundle/Twig/TagExtension.php index 7163cb88d66..dda222808b9 100644 --- a/src/Oro/Bundle/TagBundle/Twig/TagExtension.php +++ b/src/Oro/Bundle/TagBundle/Twig/TagExtension.php @@ -2,12 +2,8 @@ namespace Oro\Bundle\TagBundle\Twig; -use Oro\Bundle\UserBundle\Acl\ManagerInterface; use Symfony\Component\Routing\Router; -use Symfony\Component\Security\Core\SecurityContextInterface; -use Oro\Bundle\TagBundle\Entity\Tag; -use Oro\Bundle\TagBundle\Entity\Tagging; use Oro\Bundle\TagBundle\Entity\Taggable; use Oro\Bundle\TagBundle\Entity\TagManager; @@ -18,38 +14,9 @@ class TagExtension extends \Twig_Extension */ protected $manager; - /** - * @var Router - */ - protected $router; - - /** - * @var SecurityContextInterface - */ - protected $context; - - /** - * @var \Oro\Bundle\UserBundle\Acl\ManagerInterface - */ - private $aclManager; - - public function __construct(TagManager $manager, Router $router, SecurityContextInterface $securityContext, ManagerInterface $aclManager) + public function __construct(TagManager $manager) { $this->manager = $manager; - $this->router = $router; - $this->aclManager = $aclManager; - - $this->context = $securityContext; - } - - /** - * Return current user - * - * @return mixed - */ - public function getUser() - { - return $this->context->getToken()->getUser(); } /** @@ -70,39 +37,7 @@ public function getFunctions() */ public function get(Taggable $entity) { - $this->manager->loadTagging($entity); - $result = array(); - - /** @var Tag $tag */ - foreach ($entity->getTags() as $tag) { - $entry = array( - 'name' => $tag->getName(), - 'id' => $tag->getId(), - 'url' => $this->router->generate('oro_tag_search', array('id' => $tag->getId())) - ); - - $taggingCollection = $tag->getTagging()->filter( - function (Tagging $tagging) use ($entity) { - // only use tagging entities that related to current entity - return $tagging->getEntityName() == get_class($entity) - && $tagging->getRecordId() == $entity->getTaggableId(); - } - ); - /** @var Tagging $tagging */ - foreach ($taggingCollection as $tagging) { - if ($this->getUser()->getId() == $tagging->getCreatedBy()->getId()) { - $entry['owner'] = true; - } - } - - if (!$this->aclManager->isResourceGranted('oro_tag_unassign_global') && !isset($entry['owner'])) { - $entry['locked'] = true; - } - - $result[] = $entry; - } - - return $result; + return $this->manager->getPreparedArray($entity); } /** From a14dbd03119982c66dfb945c1f81c26d10ad976f Mon Sep 17 00:00:00 2001 From: kotfalya Date: Thu, 1 Aug 2013 22:36:14 +0300 Subject: [PATCH 044/541] refactoring config --- .../Config/{AbstractConfig.php => Config.php} | 53 ++++++- .../Config/ConfigInterface.php | 11 +- .../Config/EntityConfig.php | 149 ------------------ .../Config/EntityConfigInterface.php | 12 -- .../EntityConfigBundle/Config/FieldConfig.php | 147 ----------------- .../Config/FieldConfigInterface.php | 16 -- .../EntityConfigBundle/Config/Id/EntityId.php | 81 ++++++++++ .../Config/Id/EntityIdInterface.php | 11 ++ .../EntityConfigBundle/Config/Id/FieldId.php | 120 ++++++++++++++ .../Config/Id/FieldIdInterface.php | 21 +++ .../Config/Id/IdInterface.php | 21 +++ .../Resources/doc/config_provider.md | 12 +- 12 files changed, 311 insertions(+), 343 deletions(-) rename src/Oro/Bundle/EntityConfigBundle/Config/{AbstractConfig.php => Config.php} (65%) delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/FieldConfig.php delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php similarity index 65% rename from src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Config.php index abd51c44fe5..16b069a6132 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php @@ -2,15 +2,37 @@ namespace Oro\Bundle\EntityConfigBundle\Config; +use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; -abstract class AbstractConfig implements ConfigInterface +class Config implements ConfigInterface { + /** + * @var IdInterface + */ + protected $id; + /** * @var array */ protected $values = array(); + /** + * @param IdInterface $id + */ + public function __construct(IdInterface $id) + { + $this->id = $id; + } + + /** + * @return IdInterface + */ + public function getId() + { + return $this->id; + } + /** * @param $code * @param bool $strict @@ -22,10 +44,7 @@ public function get($code, $strict = false) if (isset($this->values[$code])) { return $this->values[$code]; } elseif ($strict) { - throw new RuntimeException(sprintf( - "Config '%s' for class '%s' in scope '%s' is not found ", - $code, $this->getClassName(), $this->getScope() - )); + throw new RuntimeException(sprintf('Value "%s" for %s', $code, $this->getId())); } return null; @@ -58,7 +77,7 @@ public function has($code) */ public function is($code) { - return (bool) $this->get($code); + return (bool)$this->get($code); } /** @@ -90,4 +109,26 @@ public function setValues($values) return $this; } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->id, + $this->values, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->id, + $this->values, + ) = unserialize($serialized); + } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php index 040b2a98972..cf29dbbe7e2 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php @@ -2,17 +2,14 @@ namespace Oro\Bundle\EntityConfigBundle\Config; +use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; + interface ConfigInterface extends \Serializable { /** - * @return string - */ - public function getClassName(); - - /** - * @return string + * @return IdInterface */ - public function getScope(); + public function getId(); /** * @param $code diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php deleted file mode 100644 index 572a4f55ff5..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php +++ /dev/null @@ -1,149 +0,0 @@ -fields = new ArrayCollection(); - $this->className = $className; - $this->scope = $scope; - } - - /** - * @param string $name - * @return $this - */ - public function setClassName($name) - { - $this->className = $name; - - return $this; - } - - /** - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * @return string|void - */ - public function getScope() - { - return $this->scope; - } - - /** - * @param FieldConfig[] $fields - * @return $this - */ - public function setFields($fields) - { - foreach ($fields as $field) { - $this->addField($field); - } - - return $this; - } - - /** - * @param FieldConfig $field - */ - public function addField(FieldConfig $field) - { - $this->fields[$field->getCode()] = $field; - } - - /** - * @param $name - * @return FieldConfig - */ - public function getField($name) - { - return $this->fields[$name]; - } - - /** - * @param $name - * @return bool - */ - public function hasField($name) - { - return isset($this->fields[$name]); - } - - /** - * @param callable $filter - * @return FieldConfig[]|ArrayCollection - */ - public function getFields(\Closure $filter = null) - { - return $filter ? $this->fields->filter($filter) : $this->fields; - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - return serialize(array( - $this->className, - $this->scope, - $this->fields, - $this->values, - )); - } - - /** - * {@inheritdoc} - */ - public function unserialize($serialized) - { - list( - $this->className, - $this->scope, - $this->fields, - $this->values, - ) = unserialize($serialized); - } - - /** - * Clone Config - */ - public function __clone() - { - $this->values = array_map(function ($value) { - if (is_object($value)) { - return clone $value; - } else { - return $value; - } - }, $this->values); - - $this->fields = $this->fields->map(function ($field) { - return clone $field; - }, $this->fields); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php deleted file mode 100644 index 11d24164771..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -className = $className; - $this->code = $code; - $this->type = $type; - $this->scope = $scope; - } - - /** - * @param string $className - * @return $this - */ - public function setClassName($className) - { - $this->className = $className; - - return $this; - } - - /** - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * @param string $code - * @return $this - */ - public function setCode($code) - { - $this->code = $code; - - return $this; - } - - /** - * @return string - */ - public function getCode() - { - return $this->code; - } - - /** - * @return string - */ - public function getScope() - { - return $this->scope; - } - - /** - * @param string $type - * @return $this - */ - public function setType($type) - { - $this->type = $type; - - return $this; - } - - /** - * @return string - */ - public function getType() - { - return $this->type; - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - return serialize(array( - $this->code, - $this->className, - $this->type, - $this->scope, - $this->values, - )); - } - - /** - * {@inheritdoc} - */ - public function unserialize($serialized) - { - list( - $this->code, - $this->className, - $this->type, - $this->scope, - $this->values, - ) = unserialize($serialized); - } - - /** - * Clone Config - */ - public function __clone() - { - $this->values = array_map(function ($value) { - if (is_object($value)) { - return clone $value; - } else { - return $value; - } - }, $this->values); - - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php deleted file mode 100644 index e8e856d8e31..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -className = $className; + $this->scope = $scope; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return string + */ + public function getId() + { + return 'entity_' . $this->scope . '_' . $this->className; + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + 'Config for Entity "%s" in scope "%s"', + $this->getClassName(), + $this->getScope() + ); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->className, + $this->scope, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->className, + $this->scope, + ) = unserialize($serialized); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php new file mode 100644 index 00000000000..13c2b6dc16c --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php @@ -0,0 +1,11 @@ +className = $className; + $this->scope = $scope; + $this->fieldName = $fieldName; + $this->fieldType = $fieldType; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return string + */ + public function getFieldName() + { + return $this->fieldName; + } + + /** + * @return string + */ + public function getFieldType() + { + return $this->fieldType; + } + + /** + * @return string + */ + public function getId() + { + return 'field_' . $this->scope . '_' . $this->className . '_' . $this->fieldName; + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + 'Config for Entity "%s" Field "%s" in scope "%s"', + $this->getClassName(), + $this->getFieldName(), + $this->getScope() + ); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + $this->className, + $this->scope, + $this->fieldName, + $this->fieldType, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->className, + $this->scope, + $this->fieldName, + $this->fieldType, + ) = unserialize($serialized); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php new file mode 100644 index 00000000000..8a05fb6ea30 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php @@ -0,0 +1,21 @@ +is({parameter}) : check if parameter exists or equal to TRUE, return boolean - AbstractConfig->has({parameter}) : check if parameter exists, return boolean + Config->is({parameter}) : check if parameter exists or equal to TRUE, return boolean + Config->has({parameter}) : check if parameter exists, return boolean - AbstractConfig->get({parameter}, {strict = FALSE}) : return parameters + Config->get({parameter}, {strict = FALSE}) : return parameters - if strict == TRUE and parameters NOT exists will be Exception - if strict == FALSE and parameters NOT exists will return NULL - AbstractConfig->set({parameter}, {value}) : set parameter and return AbstractConfig + Config->set({parameter}, {value}) : set parameter and return Config Simple usage example: if ($entityAuditProvider->hasConfig(get_class($entity))) { From bda43e5235f4b2d2d0e613a8f0e5b7f51a55c780 Mon Sep 17 00:00:00 2001 From: kotfalya Date: Thu, 1 Aug 2013 22:41:28 +0300 Subject: [PATCH 045/541] refactoring config test --- .../Tests/Unit/Config/ConfigTest.php | 44 ++++++++++++ .../Tests/Unit/Config/EntityConfigTest.php | 43 ------------ .../Tests/Unit/Config/FieldConfigTest.php | 67 ------------------- .../Tests/Unit/Config/Id/EntityIdTest.php | 30 +++++++++ .../Tests/Unit/Config/Id/FieldIdTest.php | 32 +++++++++ .../Tests/Unit/Entity/ConfigTest.php | 2 +- 6 files changed, 107 insertions(+), 111 deletions(-) create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php new file mode 100644 index 00000000000..b2a6d8b968f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php @@ -0,0 +1,44 @@ +config = new Config(new EntityId('testClass', 'testScope')); + } + + public function testValueConfig() + { + $values = array('firstKey' => 'firstValue', 'secondKey' => 'secondValue'); + $this->config->setValues($values); + + $this->assertEquals($values, $this->config->getValues()); + + $this->assertEquals('firstValue', $this->config->get('firstKey')); + $this->assertEquals('secondValue', $this->config->get('secondKey')); + + $this->assertEquals(true, $this->config->is('secondKey')); + + $this->assertEquals(true, $this->config->has('secondKey')); + $this->assertEquals(false, $this->config->has('thirdKey')); + + $this->assertEquals(null, $this->config->get('thirdKey')); + + $this->config->set('secondKey', 'secondValue2'); + $this->assertEquals('secondValue2', $this->config->get('secondKey')); + + $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + $this->config->get('thirdKey', true); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php deleted file mode 100644 index 4b6596695de..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'testScope'); - } - - public function testSetClassName() - { - $this->entityConfig->setClassName('testClass'); - $this->assertEquals('testClass', $this->entityConfig->getClassName()); - } - - public function testField() - { - $fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); - - $this->entityConfig->addField($fieldConfig); - - $this->assertEquals(true, $this->entityConfig->hasField('testField')); - $this->assertEquals($fieldConfig, $this->entityConfig->getField('testField')); - - $fieldConfig2 = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField2', 'string', 'testScope'); - - $this->entityConfig->setFields(array($fieldConfig2)); - - $this->assertEquals(true, $this->entityConfig->hasField('testField2')); - $this->assertEquals($fieldConfig2, $this->entityConfig->getField('testField2')); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php deleted file mode 100644 index 54c719096a6..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php +++ /dev/null @@ -1,67 +0,0 @@ -fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); - } - - public function testGetConfig() - { - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->fieldConfig->getClassName()); - $this->assertEquals('testField', $this->fieldConfig->getCode()); - $this->assertEquals('string', $this->fieldConfig->getType()); - $this->assertEquals('testScope', $this->fieldConfig->getScope()); - } - - public function testSetConfig() - { - $this->fieldConfig->setClassName('testClass'); - $this->fieldConfig->setCode('testField2'); - $this->fieldConfig->setType('datetime'); - - $this->assertEquals('testClass', $this->fieldConfig->getClassName()); - $this->assertEquals('testField2', $this->fieldConfig->getCode()); - $this->assertEquals('datetime', $this->fieldConfig->getType()); - } - - public function testValueConfig() - { - $values = array('firstKey' => 'firstValue', 'secondKey' => 'secondValue'); - $this->fieldConfig->setValues($values); - - $this->assertEquals($values, $this->fieldConfig->getValues()); - - $this->assertEquals('firstValue', $this->fieldConfig->get('firstKey')); - $this->assertEquals('secondValue', $this->fieldConfig->get('secondKey')); - - $this->assertEquals(true, $this->fieldConfig->is('secondKey')); - - $this->assertEquals(true, $this->fieldConfig->has('secondKey')); - $this->assertEquals(false, $this->fieldConfig->has('thirdKey')); - - $this->assertEquals(null, $this->fieldConfig->get('thirdKey')); - - $this->fieldConfig->set('secondKey', 'secondValue2'); - $this->assertEquals('secondValue2', $this->fieldConfig->get('secondKey')); - - $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - $this->fieldConfig->get('thirdKey', true); - } - - public function testSerialize() - { - $this->assertEquals($this->fieldConfig, unserialize(serialize($this->fieldConfig))); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php new file mode 100644 index 00000000000..9c1da8a78a5 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php @@ -0,0 +1,30 @@ +entityId = new EntityId('testClass', 'testScope'); + } + + public function testGetConfig() + { + $this->assertEquals('testClass', $this->entityId->getClassName()); + $this->assertEquals('testScope', $this->entityId->getScope()); + $this->assertEquals('entity_testScope_testClass', $this->entityId->getId()); + } + + public function testSerialize() + { + $this->assertEquals($this->entityId, unserialize(serialize($this->entityId))); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php new file mode 100644 index 00000000000..e9830073c8f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php @@ -0,0 +1,32 @@ +fieldId = new FieldId('testClass', 'testScope', 'testField', 'string'); + } + + public function testGetConfig() + { + $this->assertEquals('testClass', $this->fieldId->getClassName()); + $this->assertEquals('testScope', $this->fieldId->getScope()); + $this->assertEquals('testField', $this->fieldId->getFieldName()); + $this->assertEquals('string', $this->fieldId->getFieldType()); + $this->assertEquals('field_testScope_testClass_fieldName', $this->fieldId->getId()); + } + + public function testSerialize() + { + $this->assertEquals($this->fieldId, unserialize(serialize($this->fieldId))); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php index fbe84ae3a52..0acdf65ed22 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -132,7 +132,7 @@ public function test() $this->configValue->toArray() ); - /** test AbstractConfig setValues() */ + /** test Config setValues() */ $this->configEntity->setValues(array($this->configValue)); $this->assertEquals( $this->configValue, From 46b3e884738c08c0ec1830e1b648f21167906ffc Mon Sep 17 00:00:00 2001 From: kotfalya Date: Thu, 1 Aug 2013 22:59:26 +0300 Subject: [PATCH 046/541] refactoring config --- src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php | 2 +- .../EntityConfigBundle/Config/Id/EntityIdInterface.php | 4 ---- src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php | 2 +- .../EntityConfigBundle/Config/Id/FieldIdInterface.php | 5 ----- src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php | 5 +++++ .../Tests/Unit/Config/Id/EntityIdTest.php | 6 +++--- .../EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php | 6 +++--- 7 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php index 20bce10b22a..209933fbd53 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php @@ -42,7 +42,7 @@ public function getScope() */ public function getId() { - return 'entity_' . $this->scope . '_' . $this->className; + return 'entity_' . $this->scope . '_' . strtr($this->className, '\\', '-'); } /** diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php index 13c2b6dc16c..4474362e2b0 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php @@ -4,8 +4,4 @@ interface EntityIdInterface extends IdInterface { - /** - * @return string - */ - public function getClassName(); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php index 2c99810c134..f3de49e8fa2 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php @@ -76,7 +76,7 @@ public function getFieldType() */ public function getId() { - return 'field_' . $this->scope . '_' . $this->className . '_' . $this->fieldName; + return 'field_' . $this->scope . '_' . strtr($this->className, '\\', '-') . '_' . $this->fieldName; } /** diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php index 8a05fb6ea30..d2b52bd3ba4 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php @@ -4,11 +4,6 @@ interface FieldIdInterface extends IdInterface { - /** - * @return string - */ - public function getClassName(); - /** * @return string */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php index 8637c913b87..936dd00aa2f 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php @@ -9,6 +9,11 @@ interface IdInterface extends \Serializable */ public function getId(); + /** + * @return string + */ + public function getClassName(); + /** * @return string */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php index 9c1da8a78a5..e98e638fd05 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php @@ -13,14 +13,14 @@ class EntityIdTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->entityId = new EntityId('testClass', 'testScope'); + $this->entityId = new EntityId('Test\Class', 'testScope'); } public function testGetConfig() { - $this->assertEquals('testClass', $this->entityId->getClassName()); + $this->assertEquals('Test\Class', $this->entityId->getClassName()); $this->assertEquals('testScope', $this->entityId->getScope()); - $this->assertEquals('entity_testScope_testClass', $this->entityId->getId()); + $this->assertEquals('entity_testScope_Test_Class', $this->entityId->getId()); } public function testSerialize() diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php index e9830073c8f..9a24c3ac63e 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php @@ -13,16 +13,16 @@ class FieldIdTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->fieldId = new FieldId('testClass', 'testScope', 'testField', 'string'); + $this->fieldId = new FieldId('Test\Class', 'testScope', 'testField', 'string'); } public function testGetConfig() { - $this->assertEquals('testClass', $this->fieldId->getClassName()); + $this->assertEquals('Test\Class', $this->fieldId->getClassName()); $this->assertEquals('testScope', $this->fieldId->getScope()); $this->assertEquals('testField', $this->fieldId->getFieldName()); $this->assertEquals('string', $this->fieldId->getFieldType()); - $this->assertEquals('field_testScope_testClass_fieldName', $this->fieldId->getId()); + $this->assertEquals('field_testScope_Test_Class_fieldName', $this->fieldId->getId()); } public function testSerialize() From b09991fce4b3c2d59c8e0c009d1b694f84d02516 Mon Sep 17 00:00:00 2001 From: kotfalya Date: Thu, 1 Aug 2013 23:08:15 +0300 Subject: [PATCH 047/541] refactoring config cache --- .../Cache/CacheInterface.php | 19 +++++++------- .../EntityConfigBundle/Cache/FileCache.php | 25 +++++++++---------- .../EntityConfigBundle/Config/Config.php | 4 +-- .../Config/ConfigInterface.php | 2 +- .../Tests/Unit/Cache/FileCacheTest.php | 23 +++++++++-------- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php index 3a45b4ea511..987b84401fd 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php @@ -2,25 +2,24 @@ namespace Oro\Bundle\EntityConfigBundle\Cache; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; +use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; interface CacheInterface { /** - * @param $className - * @param $scope - * @return EntityConfig|null + * @param IdInterface $configId + * @return ConfigInterface|null */ - public function loadConfigFromCache($className, $scope); + public function loadConfigFromCache(IdInterface $configId); /** - * @param EntityConfig $config + * @param ConfigInterface $config */ - public function putConfigInCache(EntityConfig $config); + public function putConfigInCache(ConfigInterface $config); /** - * @param $className - * @param $scope + * @param IdInterface $configId */ - public function removeConfigFromCache($className, $scope); + public function removeConfigFromCache(IdInterface $configId); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php b/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php index d43e0f77e87..2d90d702a49 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php +++ b/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php @@ -2,7 +2,8 @@ namespace Oro\Bundle\EntityConfigBundle\Cache; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; +use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; class FileCache implements CacheInterface { @@ -21,13 +22,12 @@ public function __construct($dir) } /** - * @param $className - * @param $scope - * @return EntityConfig + * @param IdInterface $configId + * @return ConfigInterface */ - public function loadConfigFromCache($className, $scope) + public function loadConfigFromCache(IdInterface $configId) { - $path = $this->dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; + $path = $this->dir . '/' . $configId->getId() . '.cache.php'; if (!file_exists($path)) { return null; } @@ -36,21 +36,20 @@ public function loadConfigFromCache($className, $scope) } /** - * @param EntityConfig $config + * @param ConfigInterface $config */ - public function putConfigInCache(EntityConfig $config) + public function putConfigInCache(ConfigInterface $config) { - $path = $this->dir . '/' . strtr($config->getClassName(), '\\', '-') . '.' . $config->getScope() . '.cache.php'; + $path = $this->dir . '/' . $config->getConfigId()->getId() . '.cache.php'; file_put_contents($path, 'dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; + $path = $this->dir . '/' . $configId->getId() . '.cache.php'; if (file_exists($path)) { unlink($path); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php index 16b069a6132..a817c8f33ce 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php @@ -28,7 +28,7 @@ public function __construct(IdInterface $id) /** * @return IdInterface */ - public function getId() + public function getConfigId() { return $this->id; } @@ -44,7 +44,7 @@ public function get($code, $strict = false) if (isset($this->values[$code])) { return $this->values[$code]; } elseif ($strict) { - throw new RuntimeException(sprintf('Value "%s" for %s', $code, $this->getId())); + throw new RuntimeException(sprintf('Value "%s" for %s', $code, $this->getConfigId())); } return null; diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php index cf29dbbe7e2..eb864d451e4 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php @@ -9,7 +9,7 @@ interface ConfigInterface extends \Serializable /** * @return IdInterface */ - public function getId(); + public function getConfigId(); /** * @param $code diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php index cf298811e9b..e3ec43fa1bb 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php @@ -3,8 +3,8 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Cache; use Oro\Bundle\EntityConfigBundle\Cache\FileCache; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; -use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; +use Oro\Bundle\EntityConfigBundle\Config\Config; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityId; class FileCacheTest extends \PHPUnit_Framework_TestCase { @@ -15,6 +15,8 @@ class FileCacheTest extends \PHPUnit_Framework_TestCase private $testConfig; + private $testConfigId; + private $cacheDir; protected function setUp() @@ -22,8 +24,9 @@ protected function setUp() $this->cacheDir = sys_get_temp_dir() . '/__phpunit__config_file_cache'; mkdir($this->cacheDir); - $this->testConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'test'); - $this->fileCache = new FileCache($this->cacheDir); + $this->testConfigId = new EntityId('Test/Class', 'testScope'); + $this->testConfig = new Config($this->testConfigId); + $this->fileCache = new FileCache($this->cacheDir); } protected function tearDown() @@ -33,16 +36,16 @@ protected function tearDown() public function testCache() { - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $result = $this->fileCache->loadConfigFromCache($this->testConfigId); $this->assertEquals(null, $result); $this->fileCache->putConfigInCache($this->testConfig); - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $result = $this->fileCache->loadConfigFromCache($this->testConfigId); $this->assertEquals($this->testConfig, $result); - $this->fileCache->removeConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); + $this->fileCache->removeConfigFromCache($this->testConfigId); + $result = $this->fileCache->loadConfigFromCache($this->testConfigId); $this->assertEquals(null, $result); } @@ -50,13 +53,13 @@ public function testExceptionNotFoundDirectory() { $cacheDir = '/__phpunit__config_file_cache_wrong'; $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" does not exist.', $cacheDir)); - $this->fileCache = new FileCache($cacheDir); + $this->fileCache = new FileCache($cacheDir); } public function testExceptionNotWritableDirectory() { $cacheDir = '/'; $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" is not writable.', $cacheDir)); - $this->fileCache = new FileCache($cacheDir); + $this->fileCache = new FileCache($cacheDir); } } From e1b93ddb60848ac3af425b817235b36d0a9a1535 Mon Sep 17 00:00:00 2001 From: kotfalya Date: Thu, 1 Aug 2013 23:14:49 +0300 Subject: [PATCH 048/541] refactoring config, clone function for Config.php --- .../Bundle/EntityConfigBundle/Config/Config.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php index a817c8f33ce..2d76ea2c768 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php @@ -131,4 +131,19 @@ public function unserialize($serialized) $this->values, ) = unserialize($serialized); } + + /** + * Clone Config + */ + public function __clone() + { + $this->id = clone $this->id; + $this->values = array_map(function ($value) { + if (is_object($value)) { + return clone $value; + } else { + return $value; + } + }, $this->values); + } } From 3f36d2efb84dee776fe57f8022467aff8022ade4 Mon Sep 17 00:00:00 2001 From: Dmitry Khrysev Date: Fri, 2 Aug 2013 01:04:00 +0300 Subject: [PATCH 049/541] CRM-328: Exrtract abstract JS widget functionality - refactored dialog - added abstract widget - started block widget --- .../UIBundle/Resources/config/assets.yml | 4 + .../public/js/backbone/widget/abstract.js | 196 ++++++++++++++++++ .../public/js/backbone/widget/block.js | 64 ++++++ .../public/js/backbone/widget/manager.js | 21 ++ .../Resources/public/js/views/dialog.js | 172 ++++----------- 5 files changed, 330 insertions(+), 127 deletions(-) create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js create mode 100644 src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js diff --git a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml index 724fc254d0a..6c44ad6dbd6 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml @@ -44,6 +44,10 @@ js: - '@OroUIBundle/Resources/public/js/form_buttons.js' 'js_routes': - 'js/routes.js' + 'widgets': + - '@OroUIBundle/Resources/public/js/backbone/widget/manager.js' + - '@OroUIBundle/Resources/public/js/backbone/widget/abstract.js' + - '@OroUIBundle/Resources/public/js/backbone/widget/block.js' css: 'UI': - 'bundles/oroui/css/less/main.less' diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js new file mode 100644 index 00000000000..b56bb5eab9d --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js @@ -0,0 +1,196 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Abstract = Backbone.View.extend({ + options: { + type: 'widget', + actionsEl: '.widget-actions', + url: false, + elementFirst: true, + title: '' + }, + + setTitle: function(title) { + console.warn('Implement setTitle'); + }, + + show: function() { + console.warn('Implement show'); + }, + + renderActions: function() { + console.warn('Implement renderActions'); + }, + + /** + * Initialize + */ + initializeWidget: function(options) { + this.on('adoptedFormSubmitClick', _.bind(this._onAdoptedFormSubmitClick, this)); + this.on('adoptedFormResetClick', _.bind(this._onAdoptedFormResetClick, this)); + this.on('adoptedFormSubmit', _.bind(this._onAdoptedFormSubmit, this)); + + this.actions = []; + this.firstRun = true; + this.widgetContent = this.$el; + }, + + getWid: function() { + if (!this._wid) { + this._wid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } + return this._wid; + }, + + _initEmbeddedForm: function() { + var adoptedActions = this._getAdoptedActionsContainer(); + this.hasAdoptedActions = adoptedActions.length > 0; + if (this.hasAdoptedActions) { + var form = adoptedActions.closest('form'); + + if (form.length > 0) { + this.form = form; + var formAction = this.form.attr('action'); + if (formAction.length > 0 && formAction[0] != '#') { + this.options.url = formAction; + } + } + } + }, + + /** + * Move form actions to widget actions + */ + adoptFormActions: function() { + this._initEmbeddedForm(); + if (this.form !== undefined) { + var actions = this._getActionsElement(); + var self = this; + actions.find('[type=submit]').each(function(idx, btn) { + $(btn).click(function() { + self.trigger('adoptedFormSubmitClick', self.form, self); + return false; + }); + }); + this.form.submit(function() { + self.trigger('adoptedFormSubmit', self.form, self); + return false; + }); + actions.find('[type=reset]').each(function(idx, btn) { + $(btn).click(function() { + self.trigger('adoptedFormResetClick', self.form, self); + }); + }); + actions.show(); + } + }, + + _onAdoptedFormSubmitClick: function(form) + { + form.submit(); + }, + + _onAdoptedFormSubmit: function(form) + { + this.loadContent(form.serialize(), form.attr('method')); + }, + + _onAdoptedFormResetClick: function(form) + { + $(form).trigger('reset'); + }, + + /** + * Get form buttons + * + * @returns {(*|jQuery|HTMLElement)} + * @private + */ + _getActionsElement: function() { + if (!this.actionsEl) { + this.actionsEl = this._getAdoptedActionsContainer() || document.createElement('div'); + } + return this.actionsEl; + }, + + _getAdoptedActionsContainer: function() { + if (this.options.actionsEl !== undefined) { + if (typeof this.options.actionsEl == 'string') { + return this.widgetContent.find(this.options.actionsEl); + } else if (_.isElement(this.options.actionsEl )) { + return this.options.actionsEl; + } + } + return false; + }, + + addAction: function(actionElement) { + this.actions.push(actionElement); + this.renderActions(); + }, + + getPreparedActions: function() { + this.adoptFormActions(); + var container = this._getActionsElement(); + for (var i = 0; i < this.actions.length; i++) { + container.append(this.actions[i]); + } + return container; + }, + + /** + * Render widget + */ + render: function() { + var loadAllowed = this.$el.html().length == 0 || !this.options.elementFirst + || (this.options.elementFirst && !this.firstRun); + if (loadAllowed && this.options.url !== false) { + this.loadContent(); + } else { + this.show(); + this.trigger('render', this.$el, this); + } + this.firstRun = false; + }, + + /** + * Load content + * + * @param {Object|null} data + * @param {String|null} method + */ + loadContent: function(data, method) { + var url = this.options.url; + if (url === undefined || !url) { + url = window.location.href; + } + if (this.firstRun || method === undefined || !method) { + method = 'get'; + } + var options = { + url: url, + type: method + }; + if (data !== undefined) { + options.data = data; + } + options.data = (options.data !== undefined ? options.data + '&' : '') + + '_widgetContainer=' + this.options.type + '&_wid=' + this.getWid(); + + Backbone.$.ajax(options).done(_.bind(function(content) { + try { + this.trigger('contentLoad', content, this); + this.actionsEl = null; + this.widgetContent = $('
      ').html(content); + this.show(); + this.trigger('render', this.$el, this); + } catch (error) { + // Remove state with unrestorable content + this.trigger('contentLoadError', this); + } + }, this)); + }, +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js new file mode 100644 index 00000000000..4681d5bb646 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js @@ -0,0 +1,64 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Block = Oro.widget.Abstract.extend({ + options: _.extend( + Oro.widget.Abstract.prototype.options, + { + type: 'block', + titleEl: '.widget-title', + actionsEl: '.widget-actions', + contentEl: '.box-content', + template: _.template('
      ' + + '
      ' + + '
      ' + + '<%- title %>' + + '
      ' + + '
      ' + + '
      ') + } + ), + + initialize: function(options) { + options = options || {} + this.initializeWidget(options); + + var anchorDiv = $('
      '); + anchorDiv.after(this.$el); + this.widget = Backbone.$(this.options.template({ + 'title': this.options.title + })); + this.widgetContent = this.widget.find(this.options.contentEl); + this.widgetContent.append(this.$el); + anchorDiv.replaceWith(this.widget); + }, + + setTitle: function(title) { + this.options.title = title; + this._getTitleContainer().html(this.options.title); + }, + + renderActions: function() { + this._getActionsContainer().append(this.getPreparedActions()); + }, + + _getActionsContainer: function() { + if (this.actionsContainer === undefined) { + this.actionsContainer = this.widget.find(this.options.actionsEl); + } + return this.actionsContainer; + }, + + _getTitleContainer: function() { + if (this.titleContainer === undefined) { + this.titleContainer = this.widget.find(this.options.titleEl); + } + return this.titleContainer; + }, + + show: function() { + this.renderActions(); + } +}); + +Oro.widget.Manager.registerWidgetContainer('block', Oro.widget.Block); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js new file mode 100644 index 00000000000..1057bd2a443 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js @@ -0,0 +1,21 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Manager = { + types: {}, + widgets: {}, + + registerWidgetContainer: function(type, initializer) { + this.types[type] = initializer; + }, + + createWidget: function(type, options) { + var widget = new this.types[type](options); + this.widgets[widget.getWid()] = widget; + return widget; + }, + + getWidgetInstance: function(wid) { + return this.widgets[wid]; + } +}; diff --git a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js index 731aa8e072f..cbefa380e75 100644 --- a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js +++ b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js @@ -1,16 +1,14 @@ var Oro = Oro || {}; Oro.widget = Oro.widget || {}; -Oro.widget.DialogView = Backbone.View.extend({ - options: { - type: 'dialog', - actionsEl: '.widget-actions', - dialogOptions: null, - url: false, - elementFirst: true - }, - actions: null, - firstRun: true, +Oro.widget.DialogView = Oro.widget.Abstract.extend({ + options: _.extend( + Oro.widget.Abstract.prototype.options, + { + type: 'dialog', + dialogOptions: null + } + ), // Windows manager global variables windowsPerRow: 10, @@ -26,13 +24,18 @@ Oro.widget.DialogView = Backbone.View.extend({ */ initialize: function(options) { options = options || {} - options.dialogOptions = options.dialogOptions || {}; - options.dialogOptions.limitTo = options.dialogOptions.limitTo || '#container'; + this.initializeWidget(options); + + this.on('adoptedFormResetClick', _.bind(function() { + this.widget.dialog('close') + }, this)); - this._initModel(options); + this.options.dialogOptions = this.options.dialogOptions || {}; + this.options.dialogOptions.title = this.options.dialogOptions.title || this.options.title; + this.options.dialogOptions.limitTo = this.options.dialogOptions.limitTo || '#container'; - this.dialogContent = this.$el; - this._initEmbeddedForm(); + this._initModel(this.options); + this.widgetContent = this.$el; var runner = function(handlers) { return function() { @@ -43,7 +46,19 @@ Oro.widget.DialogView = Backbone.View.extend({ } } }; - this.options.dialogOptions.close = runner([_.bind(this.closeHandler, this), this.options.dialogOptions.close]); + + var closeHandlers = [_.bind(this.closeHandler, this)]; + if (this.options.dialogOptions.close !== undefined) { + closeHandlers.push(this.options.dialogOptions.close); + } + + this.options.dialogOptions.close = runner(closeHandlers); + + this.on('contentLoadError', _.bind(this.loadErrorHandler, this)); + }, + + setTitle: function(title) { + this.widget.dialog("option", "title", title); }, _initModel: function(options) { @@ -68,50 +83,6 @@ Oro.widget.DialogView = Backbone.View.extend({ } }, - _initEmbeddedForm: function() { - this.hasAdoptedActions = this._getActionsElement().length > 0; - if (this.hasAdoptedActions) { - this.form = this._getActionsElement().closest('form'); - - var formAction = this.form.attr('action'); - if (formAction.length > 0 && formAction[0] != '#') { - this.options.url = formAction; - } - } - }, - - /** - * Move form actions to dialog - */ - adoptActions: function() { - if (this.hasAdoptedActions) { - var actions = this._getActionsElement(); - var self = this; - actions.find('[type=submit]').each(function(idx, btn) { - $(btn).click(function() { - self.form.submit(); - return false; - }); - }); - this.form.submit(function() { - self.loadContent(self.form.serialize(), self.form.attr('method')); - return false; - }); - actions.find('[type=reset]').each(function(idx, btn) { - $(btn).click(function() { - $(self.form).trigger('reset'); - self.widget.dialog('close'); - }); - }); - actions.show(); - - var container = this.widget.dialog('actionsContainer'); - container.empty(); - this._getActionsElement().appendTo(container); - this.widget.dialog('showActionsContainer'); - } - }, - /** * Handle dialog close */ @@ -124,7 +95,7 @@ Oro.widget.DialogView = Backbone.View.extend({ } }, this) }); - this.dialogContent.remove(); + this.widgetContent.remove(); this._getActionsElement().remove(); }, @@ -151,22 +122,6 @@ Oro.widget.DialogView = Backbone.View.extend({ this.model.save({data: saveData}); }, - /** - * Get form buttons - * - * @returns {(*|jQuery|HTMLElement)} - * @private - */ - _getActionsElement: function() { - if (!this.actions) { - this.actions = this.options.actionsEl; - if (typeof this.actions == 'string') { - this.actions = this.dialogContent.find(this.actions); - } - } - return this.actions; - }, - close: function() { this.widget.dialog('close'); }, @@ -175,54 +130,16 @@ Oro.widget.DialogView = Backbone.View.extend({ return this.widget; }, - /** - * Render dialog - */ - render: function() { - var loadAllowed = this.$el.html().length == 0 || !this.options.elementFirst || (this.options.elementFirst && !this.firstRun); - if (loadAllowed && this.options.url !== false) { - this.loadContent(); - } else { - this.show(); - } - this.firstRun = false; + loadErrorHandler: function() + { + this.model.destroy(); }, - /** - * Load dialog content - * - * @param {Object|null} data - * @param {String|null} method - */ - loadContent: function(data, method) { - var url = this.options.url; - if (typeof url == 'undefined' || !url) { - url = window.location.href; - } - if (this.firstRun || typeof method == 'undefined' || !method) { - method = 'get'; - } - var options = { - url: url, - type: method - }; - if (typeof data != 'undefined') { - options.data = data; - } - options.data = (typeof options.data != 'undefined' ? options.data + '&' : '') - + '_widgetContainer=' + this.options.type; - - Backbone.$.ajax(options).done(_.bind(function(content) { - try { - this.actions = null; - this.dialogContent = $('
      ').html(content); - this._initEmbeddedForm(); - this.show(); - } catch (error) { - // Remove state with unrestorable content - this.model.destroy(); - } - }, this)); + renderActions: function() { + var container = this.widget.dialog('actionsContainer'); + container.empty(); + this.getPreparedActions().appendTo(container); + this.widget.dialog('showActionsContainer'); }, /** @@ -234,12 +151,11 @@ Oro.widget.DialogView = Backbone.View.extend({ this.options.dialogOptions.position = this._getWindowPlacement(); } this.options.dialogOptions.stateChange = _.bind(this.handleStateChange, this); - this.widget = this.dialogContent.dialog(this.options.dialogOptions); + this.widget = this.widgetContent.dialog(this.options.dialogOptions); } else { - this.widget.html(this.dialogContent); + this.widget.html(this.widgetContent); } - - this.adoptActions(); + this.renderActions(); }, /** @@ -268,3 +184,5 @@ Oro.widget.DialogView = Backbone.View.extend({ }; } }); + +Oro.widget.Manager.registerWidgetContainer('dialog', Oro.widget.DialogView); From 3d6a7b3bab7c0f9170fea4dcc350bc8ead11bbcb Mon Sep 17 00:00:00 2001 From: kotfalya Date: Fri, 2 Aug 2013 01:53:11 +0300 Subject: [PATCH 050/541] refactoring config, rename Id to ConfigId --- src/Oro/Bundle/EntityConfigBundle/Config/Config.php | 10 +++++----- .../EntityConfigBundle/Config/ConfigInterface.php | 4 ++-- .../Id/{IdInterface.php => ConfigIdInterface.php} | 2 +- .../Config/Id/{EntityId.php => EntityConfigId.php} | 2 +- ...tityIdInterface.php => EntityConfigIdInterface.php} | 2 +- .../Config/Id/{FieldId.php => FieldConfigId.php} | 2 +- ...FieldIdInterface.php => FieldConfigIdInterface.php} | 2 +- .../Controller/ConfigFieldGridController.php | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename src/Oro/Bundle/EntityConfigBundle/Config/Id/{IdInterface.php => ConfigIdInterface.php} (87%) rename src/Oro/Bundle/EntityConfigBundle/Config/Id/{EntityId.php => EntityConfigId.php} (95%) rename src/Oro/Bundle/EntityConfigBundle/Config/Id/{EntityIdInterface.php => EntityConfigIdInterface.php} (51%) rename src/Oro/Bundle/EntityConfigBundle/Config/Id/{FieldId.php => FieldConfigId.php} (97%) rename src/Oro/Bundle/EntityConfigBundle/Config/Id/{FieldIdInterface.php => FieldConfigIdInterface.php} (78%) diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php index 2d76ea2c768..227b0e72515 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php @@ -2,13 +2,13 @@ namespace Oro\Bundle\EntityConfigBundle\Config; -use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; class Config implements ConfigInterface { /** - * @var IdInterface + * @var ConfigIdInterface */ protected $id; @@ -18,15 +18,15 @@ class Config implements ConfigInterface protected $values = array(); /** - * @param IdInterface $id + * @param ConfigIdInterface $id */ - public function __construct(IdInterface $id) + public function __construct(ConfigIdInterface $id) { $this->id = $id; } /** - * @return IdInterface + * @return ConfigIdInterface */ public function getConfigId() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php index eb864d451e4..81014f1af66 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php @@ -2,12 +2,12 @@ namespace Oro\Bundle\EntityConfigBundle\Config; -use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; interface ConfigInterface extends \Serializable { /** - * @return IdInterface + * @return ConfigIdInterface */ public function getConfigId(); diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php similarity index 87% rename from src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php index 936dd00aa2f..54003388064 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/IdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Config\Id; -interface IdInterface extends \Serializable +interface ConfigIdInterface extends \Serializable { /** * @return string diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php similarity index 95% rename from src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php index 209933fbd53..7518d0e812a 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Config\Id; -class EntityId implements EntityIdInterface +class EntityConfigId implements EntityConfigIdInterface { /** * @var string diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php similarity index 51% rename from src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php index 4474362e2b0..81bd383c2cf 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityIdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php @@ -2,6 +2,6 @@ namespace Oro\Bundle\EntityConfigBundle\Config\Id; -interface EntityIdInterface extends IdInterface +interface EntityConfigIdInterface extends ConfigIdInterface { } diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php similarity index 97% rename from src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php index f3de49e8fa2..823b591e977 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Config\Id; -class FieldId implements FieldIdInterface +class FieldConfigId implements FieldConfigIdInterface { /** * @var string diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php similarity index 78% rename from src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php rename to src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php index d2b52bd3ba4..b53e64dba8f 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldIdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Config\Id; -interface FieldIdInterface extends IdInterface +interface FieldConfigIdInterface extends ConfigIdInterface { /** * @return string diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php index 45e28488c7d..60fc92ed9b5 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php @@ -124,7 +124,7 @@ public function removeAction(ConfigField $field) $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); $fieldConfig = $extendManager->getConfigProvider() - ->getFieldConfig($field->getEntity()->getClassName(), $field->getCode()); + ->getFieldConfig($field->getEntity()->getClassName(), $field->getFieldName()); if (!$fieldConfig->is('is_extend')) { return new Response('', Codes::HTTP_FORBIDDEN); } From 4120ff0e2d80410f1be61a8aa15e1e3146ebb6b9 Mon Sep 17 00:00:00 2001 From: kotfalya Date: Fri, 2 Aug 2013 01:55:14 +0300 Subject: [PATCH 051/541] refactoring entity, rename code to fieldName --- .../Entity/ConfigEntity.php | 8 +++--- .../EntityConfigBundle/Entity/ConfigField.php | 26 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php index 4e26aff3d8e..37025d73132 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php @@ -108,13 +108,13 @@ public function getFields(\Closure $filter = null) } /** - * @param $code + * @param $fieldName * @return ConfigField */ - public function getField($code) + public function getField($fieldName) { - $fields = $this->getFields(function (ConfigField $field) use ($code) { - return $field->getCode() == $code; + $fields = $this->getFields(function (ConfigField $field) use ($fieldName) { + return $field->getFieldName() == $fieldName; }); return $fields->first(); diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php index d6496e21888..ab39ee95387 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php @@ -19,7 +19,7 @@ class ConfigField extends AbstractConfig /** * @var integer - * @ORM\Column(name="id", type="integer", nullable=false) + * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ @@ -42,9 +42,9 @@ class ConfigField extends AbstractConfig /** * @var string - * @ORM\Column(type="string", length=255, nullable=false) + * @ORM\Column(name="field_name", type="string", length=255) */ - protected $code; + protected $fieldName; /** * @var string @@ -52,12 +52,12 @@ class ConfigField extends AbstractConfig */ protected $type; - public function __construct($code = null, $type = null) + public function __construct($fieldName = null, $type = null) { - $this->code = $code; - $this->type = $type; - $this->values = new ArrayCollection; - $this->mode = self::MODE_VIEW_DEFAULT; + $this->fieldName = $fieldName; + $this->type = $type; + $this->values = new ArrayCollection; + $this->mode = self::MODE_VIEW_DEFAULT; } /** @@ -69,12 +69,12 @@ public function getId() } /** - * @param string $code + * @param string $fieldName * @return $this */ - public function setCode($code) + public function setFieldName($fieldName) { - $this->code = $code; + $this->fieldName = $fieldName; return $this; } @@ -82,9 +82,9 @@ public function setCode($code) /** * @return string */ - public function getCode() + public function getFieldName() { - return $this->code; + return $this->fieldName; } /** From 640798c3fa71b596173592d6cd0ca35639f1cb63 Mon Sep 17 00:00:00 2001 From: kotfalya Date: Fri, 2 Aug 2013 01:56:10 +0300 Subject: [PATCH 052/541] refactoring config, rename Id to ConfigId refactoring entity, rename code to fieldName --- .../Bundle/EntityConfigBundle/Cache/CacheInterface.php | 10 +++++----- src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php | 10 +++++----- .../Tests/Unit/Cache/FileCacheTest.php | 4 ++-- .../Tests/Unit/Config/ConfigTest.php | 4 ++-- .../Tests/Unit/Config/Id/EntityIdTest.php | 6 +++--- .../Tests/Unit/Config/Id/FieldIdTest.php | 6 +++--- .../Tests/Unit/Entity/ConfigTest.php | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php index 987b84401fd..74960277756 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php @@ -3,15 +3,15 @@ namespace Oro\Bundle\EntityConfigBundle\Cache; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; -use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; interface CacheInterface { /** - * @param IdInterface $configId + * @param ConfigIdInterface $configId * @return ConfigInterface|null */ - public function loadConfigFromCache(IdInterface $configId); + public function loadConfigFromCache(ConfigIdInterface $configId); /** * @param ConfigInterface $config @@ -19,7 +19,7 @@ public function loadConfigFromCache(IdInterface $configId); public function putConfigInCache(ConfigInterface $config); /** - * @param IdInterface $configId + * @param ConfigIdInterface $configId */ - public function removeConfigFromCache(IdInterface $configId); + public function removeConfigFromCache(ConfigIdInterface $configId); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php b/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php index 2d90d702a49..39e95d5af5b 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php +++ b/src/Oro/Bundle/EntityConfigBundle/Cache/FileCache.php @@ -3,7 +3,7 @@ namespace Oro\Bundle\EntityConfigBundle\Cache; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; -use Oro\Bundle\EntityConfigBundle\Config\Id\IdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; class FileCache implements CacheInterface { @@ -22,10 +22,10 @@ public function __construct($dir) } /** - * @param IdInterface $configId + * @param ConfigIdInterface $configId * @return ConfigInterface */ - public function loadConfigFromCache(IdInterface $configId) + public function loadConfigFromCache(ConfigIdInterface $configId) { $path = $this->dir . '/' . $configId->getId() . '.cache.php'; if (!file_exists($path)) { @@ -45,9 +45,9 @@ public function putConfigInCache(ConfigInterface $config) } /** - * @param IdInterface $configId + * @param ConfigIdInterface $configId */ - public function removeConfigFromCache(IdInterface $configId) + public function removeConfigFromCache(ConfigIdInterface $configId) { $path = $this->dir . '/' . $configId->getId() . '.cache.php'; if (file_exists($path)) { diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php index e3ec43fa1bb..c49b5effd48 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php @@ -4,7 +4,7 @@ use Oro\Bundle\EntityConfigBundle\Cache\FileCache; use Oro\Bundle\EntityConfigBundle\Config\Config; -use Oro\Bundle\EntityConfigBundle\Config\Id\EntityId; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; class FileCacheTest extends \PHPUnit_Framework_TestCase { @@ -24,7 +24,7 @@ protected function setUp() $this->cacheDir = sys_get_temp_dir() . '/__phpunit__config_file_cache'; mkdir($this->cacheDir); - $this->testConfigId = new EntityId('Test/Class', 'testScope'); + $this->testConfigId = new EntityConfigId('Test/Class', 'testScope'); $this->testConfig = new Config($this->testConfigId); $this->fileCache = new FileCache($this->cacheDir); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php index b2a6d8b968f..452cd2b6414 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php @@ -3,7 +3,7 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Config; use Oro\Bundle\EntityConfigBundle\Config\Config; -use Oro\Bundle\EntityConfigBundle\Config\Id\EntityId; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; class ConfigTest extends \PHPUnit_Framework_TestCase @@ -15,7 +15,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->config = new Config(new EntityId('testClass', 'testScope')); + $this->config = new Config(new EntityConfigId('testClass', 'testScope')); } public function testValueConfig() diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php index e98e638fd05..7c0f538cc5b 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php @@ -2,18 +2,18 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Config\Id; -use Oro\Bundle\EntityConfigBundle\Config\Id\EntityId; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; class EntityIdTest extends \PHPUnit_Framework_TestCase { /** - * @var EntityId + * @var EntityConfigId */ protected $entityId; public function setUp() { - $this->entityId = new EntityId('Test\Class', 'testScope'); + $this->entityId = new EntityConfigId('Test\Class', 'testScope'); } public function testGetConfig() diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php index 9a24c3ac63e..5b3b890adc8 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php @@ -2,18 +2,18 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Config\Id; -use Oro\Bundle\EntityConfigBundle\Config\Id\FieldId; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigId; class FieldIdTest extends \PHPUnit_Framework_TestCase { /** - * @var FieldId + * @var FieldConfigId */ protected $fieldId; public function setUp() { - $this->fieldId = new FieldId('Test\Class', 'testScope', 'testField', 'string'); + $this->fieldId = new FieldConfigId('Test\Class', 'testScope', 'testField', 'string'); } public function testGetConfig() diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php index 0acdf65ed22..2e0a2c18683 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -79,7 +79,7 @@ public function test() { $this->assertEquals( 'test', - $this->configField->getCode($this->configField->setCode('test')) + $this->configField->getFieldName($this->configField->setFieldName('test')) ); $this->assertEquals( From 701d6bc1de06e7d7489edfa9b6ad8be112d8a88e Mon Sep 17 00:00:00 2001 From: Alexandr Smaga Date: Thu, 1 Aug 2013 22:55:52 +0000 Subject: [PATCH 053/541] BAP-1233: Tags refactoring --- .../Bundle/TagBundle/Entity/TagManager.php | 23 +------- src/Oro/Bundle/TagBundle/Entity/Tagging.php | 2 +- .../Form/EventSubscriber/TagSubscriber.php | 55 +++++++------------ .../Form/Type/TagAutocompleteType.php | 2 - .../TagBundle/Form/Type/TagSelectType.php | 15 +---- .../TagBundle/Resources/config/services.yml | 12 ---- .../Resources/public/js/collections/tag.js | 3 +- .../Resources/public/js/models/tag.js | 7 ++- .../Resources/public/js/views/tag_update.js | 28 +++++++++- .../Resources/views/Form/fields.html.twig | 5 +- 10 files changed, 54 insertions(+), 98 deletions(-) diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index 5ed08da7bf2..eeedd008d63 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -109,25 +109,6 @@ public function removeTag(Tag $tag, Taggable $resource) return $resource->getTags()->removeElement($tag); } - /** - * @param array $ids - */ - public function loadTags(array $ids) - { - $builder = $this->em->createQueryBuilder(); - - $tags = $builder - ->select('t') - ->from($this->tagClass, 't') - - ->where($builder->expr()->in('t.id', $ids)) - - ->getQuery() - ->getResult(); - - return $tags; - } - /** * Loads or creates multiples tags from a list of tag names * @@ -162,12 +143,12 @@ public function loadOrCreateTags(array $names) if (sizeof($missingNames)) { foreach ($missingNames as $name) { $tag = $this->createTag($name); - $this->em->persist($tag); + // $this->em->persist($tag); $tags[] = $tag; } - $this->em->flush(); + // $this->em->flush(); } return $tags; diff --git a/src/Oro/Bundle/TagBundle/Entity/Tagging.php b/src/Oro/Bundle/TagBundle/Entity/Tagging.php index 488feda7850..cf520124727 100644 --- a/src/Oro/Bundle/TagBundle/Entity/Tagging.php +++ b/src/Oro/Bundle/TagBundle/Entity/Tagging.php @@ -25,7 +25,7 @@ class Tagging implements ContainAuthorInterface protected $id; /** - * @ORM\ManyToOne(targetEntity="Tag", inversedBy="tagging") + * @ORM\ManyToOne(targetEntity="Tag", inversedBy="tagging", cascade="ALL") * @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE") **/ protected $tag; diff --git a/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php index e4cbd98f930..4fab31e7d1a 100644 --- a/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php +++ b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php @@ -28,7 +28,8 @@ public static function getSubscribedEvents() { return array( FormEvents::PRE_SET_DATA => 'preSet', - FormEvents::PRE_SUBMIT => 'preSubmit' + FormEvents::PRE_SUBMIT => 'preSubmit', + FormEvents::POST_SUBMIT => 'preSet' ); } @@ -56,7 +57,7 @@ function ($item) { array( 'autocomplete' => null, 'all' => json_encode($tags), - 'owner' => json_encode($ownTags) + 'owner' => json_encode($ownTags) ) ); } @@ -72,42 +73,24 @@ public function preSubmit(FormEvent $event) 'owner' => array() ); - if (!isset($values['all'], $values['owner'])) { - return $values; - } - foreach (array_keys($entities) as $type) { - if (!is_array($values[$type])) { - $values[$type] = json_decode($values[$type]); - } - - $newValues[$type] = array_filter( - $values[$type], - function ($item) { - return !intval($item->id) && !empty($item->name); + if (isset($values[$type])) { + try { + if (!is_array($values[$type])) { + $values[$type] = json_decode($values[$type]); + } + $names[$type] = array(); + foreach ($values[$type] as $value) { + if (!empty($value->name)) { + // new tag + $names[$type][] = $value->name; + } + } + + $entities[$type] = $this->manager->loadOrCreateTags($names[$type]); + } catch (\Exception $e) { + $entities[$type] = array(); } - ); - - $newValues[$type] = array_map( - function ($item) { - return $item->name; - }, - $newValues[$type] - ); - - $values[$type] = array_map( - function ($item) { - return $item->id; - }, - $values[$type] - ); - - if ($values[$type]) { - $entities[$type] = $this->manager->loadTags($values[$type]); - } - - if ($newValues[$type]) { - $entities[$type] = array_merge($entities[$type], $this->manager->loadOrCreateTags($newValues[$type])); } } diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php index a6a2898047e..c0c49d3b40c 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php @@ -23,8 +23,6 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'autocomplete_alias' => 'tags', ) ); - - $resolver->setNormalizers(array()); } /** diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php index f6a076a531b..812ecc9b3bd 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php @@ -1,33 +1,22 @@ transformer = $transformer; $this->subscriber = $subscriber; - - $this->mapper = $mapper; } /** @@ -48,8 +37,6 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addEventSubscriber($this->subscriber); -// $builder->addViewTransformer($this->transformer); -// $builder->setDataMapper($this->mapper); $builder->add( 'autocomplete', diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index 85e86e8792b..0a5021a30e5 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -15,8 +15,6 @@ parameters: oro_tag.form.type.tag_select.class: Oro\Bundle\TagBundle\Form\Type\TagSelectType oro_tag.form.type.tag_autocomplete.class: Oro\Bundle\TagBundle\Form\Type\TagAutocompleteType oro_tag.form.subscriber.tag_select.class: Oro\Bundle\TagBundle\Form\EventSubscriber\TagSubscriber - oro_tag.form.transformer.tag_select.class: Oro\Bundle\TagBundle\Form\Transformer\TagTransformer - oro_tag.form.mapper.tag_select.class: Oro\Bundle\TagBundle\Form\DataMapper\TagMapper oro_tag.provider.search_provider.class: Oro\Bundle\TagBundle\Provider\SearchProvider @@ -119,20 +117,10 @@ services: oro_tag.form.type.tag_select: class: %oro_tag.form.type.tag_select.class% arguments: - - @oro_tag.form.transformer.tag_select - @oro_tag.form.subscriber.tag_select - - @oro_tag.form.mapper.tag_select tags: - { name: form.type, alias: oro_tag_select } - oro_tag.form.transformer.tag_select: - class: %oro_tag.form.transformer.tag_select.class% - arguments: [@oro_tag.tag.manager] - - oro_tag.form.mapper.tag_select: - class: %oro_tag.form.mapper.tag_select.class% - arguments: [@oro_tag.tag.manager] - oro_tag.form.subscriber.tag_select: class: %oro_tag.form.subscriber.tag_select.class% arguments: [@oro_tag.tag.manager] diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js index 0b2f02cf76e..e93bdb9cd9f 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js @@ -27,9 +27,8 @@ Oro.Tags.TagCollection = Backbone.Collection.extend({ * @param {Object} value */ addItem: function(value) { - var tag = new this.model({id: value.id, name: value.name}); + var tag = new this.model({id: value.id, name: value.name, owner: true, notSaved: true}); this.add(tag); - this.trigger('addItem'); } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js index e57a8411c98..7cd383b91d4 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js @@ -3,8 +3,9 @@ Oro.Tags = Oro.Tags || {}; Oro.Tags.Tag = Backbone.Model.extend({ defaults: { - owner : false, - url : '', - name : '' + owner : false, + notSaved : false, + url : '', + name : '' } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js index 1667b55ab28..ea920c93b41 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js @@ -47,9 +47,11 @@ Oro.Tags.TagsUpdateView = Oro.Tags.TagView.extend({ Oro.Tags.TagView.prototype.initialize.apply(this, arguments); + + this._renderOverlay(); this._prepareCollections(); - this._renderOverlay() - this.listenTo(this.getCollection(), 'addItem', this.render); + this.listenTo(this.getCollection(), 'add', this.render); + this.listenTo(this.getCollection(), 'add', this._updateHiddenInputs); $(this.options.autocompleteFieldId).on('change', _.bind(this._addItem, this)); }, @@ -91,7 +93,27 @@ Oro.Tags.TagsUpdateView = Oro.Tags.TagView.extend({ * @private */ _prepareCollections: function() { -// var ownTags = + try { + var allTags = $.parseJSON($(this.options.fieldId).val()); + if (! _.isArray(allTags)) { + throw new TypeError("tags hidden field data is not array") + } + } catch (e) { + allTags = []; + } + + this.getCollection().reset(allTags); + return this; + }, + + /** + * Update hidden inputs triggered by collection change + * + * @private + */ + _updateHiddenInputs: function() { + $(this.options.fieldId).val(JSON.stringify(this.getCollection())); + $(this.options.ownFieldId).val(JSON.stringify(this.getCollection('owner'))); } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index a3eae508ae3..a0a44cc9e68 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -24,14 +24,12 @@ {# show all tags #} {{ form_row(form.autocomplete) }} - {# show field with mine tags#} + {# show field with mine tags #} {% if resource_granted('oro_tag_assign_unassign') %} {{ form_row(form.autocomplete) }} {% endif %} -
      + }); + +
      + {% else %} + {{ form_row(form.all) }} + {{ form_row(form.owner) }} + {% endif %} {% endblock %} {% block oro_combobox_dataconfig_multi_autocomplete %} diff --git a/src/Oro/Bundle/UserBundle/Acl/AclInterceptor.php b/src/Oro/Bundle/UserBundle/Acl/AclInterceptor.php index 1ef4108f4e0..97178ecbd13 100644 --- a/src/Oro/Bundle/UserBundle/Acl/AclInterceptor.php +++ b/src/Oro/Bundle/UserBundle/Acl/AclInterceptor.php @@ -66,10 +66,9 @@ public function intercept(MethodInvocation $method) if (false === $this->accessDecisionManager->decide($token, $accessRoles, $method)) { //check if we have internal action - show blank - if ($this->container->get('request')->attributes->get('_route') == '_internal') { + if ($this->container->get('request')->attributes->get('_route') == null) { return new Response(''); } - throw new AccessDeniedException('Access denied.'); } } From 1d9d63bbcbc2d0deaab6703f2e48773f23630999 Mon Sep 17 00:00:00 2001 From: Michael Banin Date: Fri, 2 Aug 2013 16:45:55 +0300 Subject: [PATCH 061/541] BAP-1173 - Functional Selenium Tests for Tags ACL - Fixed tests --- .../Tests/Selenium/Tags/TagsAcl.php | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php index aa224ae76f7..7cfbeed01f4 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php @@ -109,29 +109,30 @@ public function testCreateTag() */ public function testTagAcl($aclcase, $username, $role, $tagname) { - $role = 'ROLE_NAME_' . $role; - $login = new Login($this); + $rolename = 'ROLE_NAME_' . $role; + $rolelabel = 'Label_' . $role; + $login = new Login($this); $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) ->submit(); switch ($aclcase) { case 'delete': - $this->deleteAcl($login, $role, $username, $tagname); + $this->deleteAcl($login, $rolename, $username, $tagname); break; case 'update': - $this->updateAcl($login, $role, $username, $tagname); + $this->updateAcl($login, $rolename, $username, $tagname); break; case 'create': - $this->createAcl($login, $role, $username); + $this->createAcl($login, $rolename, $username); break; case 'view list': - $this->viewListAcl($login, $role, $username); + $this->viewListAcl($login, $rolename, $username); break; case 'unassign global': - $this->unassignGlobalAcl($login, $role, $tagname); + $this->unassignGlobalAcl($login, $rolename, $rolelabel, $tagname); break; case 'assign/unassign': - $this->assignAcl($login, $role, $username); + $this->assignAcl($login, $rolename, $username); break; } } @@ -196,12 +197,12 @@ public function viewListAcl($login, $role, $username) ->assertTitle('403 - Forbidden'); } - public function unassignGlobalAcl($login, $role, $tagname) + public function unassignGlobalAcl($login, $rolename, $rolelabel, $tagname) { $username = 'user' . mt_rand(); $login->openRoles() - ->filterBy('Role', $role) - ->open(array($role)) + ->filterBy('Role', $rolename) + ->open(array($rolename)) ->selectAcl('Tag unassign global') ->save() ->openUsers() @@ -213,7 +214,7 @@ public function unassignGlobalAcl($login, $role, $tagname) ->setFirstname('First_'.$username) ->setLastname('Last_'.$username) ->setEmail($username.'@mail.com') - ->setRoles(array($role)) + ->setRoles(array($rolelabel)) ->setTag($tagname) ->save() ->logout() From 79638676988f731eaa59b7691009a947047c8277 Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Fri, 2 Aug 2013 17:18:36 +0300 Subject: [PATCH 062/541] EntityConfigBundle refactoring --- .../Command/UpdateCommand.php | 12 +- .../Config/Id/ConfigIdInterface.php | 2 + .../Config/Id/EntityConfigId.php | 8 + .../Config/Id/FieldConfigId.php | 12 + .../EntityConfigBundle/ConfigManager.php | 230 ++++++++---------- .../Controller/AuditController.php | 4 +- .../Controller/ConfigController.php | 12 +- .../Datagrid/ConfigDatagridManager.php | 4 +- .../Datagrid/EntityFieldsDatagridManager.php | 8 +- ...ractConfig.php => AbstractConfigModel.php} | 20 +- .../{ConfigValue.php => ConfigModelValue.php} | 26 +- ...ConfigEntity.php => EntityConfigModel.php} | 22 +- .../{ConfigField.php => FieldConfigModel.php} | 18 +- .../EntityConfigBundle/Event/Events.php | 10 +- .../Event/FlushConfigEvent.php | 31 --- ...ntityEvent.php => NewConfigModelEvent.php} | 20 +- .../Event/NewFieldEvent.php | 77 ------ .../Exception/LogicException.php | 7 + .../Metadata/Annotation/Configurable.php | 6 +- .../Tests/Unit/ConfigManagerTest.php | 16 +- .../Tests/Unit/Entity/ConfigTest.php | 26 +- .../Unit/Event/EntityConfigEventTest.php | 6 +- .../FoundEntityConfigRepository.php | 14 +- .../Metadata/Annotation/ConfigurableTest.php | 10 +- .../Command/GenerateCommand.php | 6 +- .../Controller/ApplyController.php | 10 +- .../Controller/ConfigEntityGridController.php | 4 +- .../Controller/ConfigFieldGridController.php | 8 +- .../EventListener/ConfigSubscriber.php | 16 +- .../EntityExtendBundle/Tools/Schema.php | 2 +- 30 files changed, 273 insertions(+), 374 deletions(-) rename src/Oro/Bundle/EntityConfigBundle/Entity/{AbstractConfig.php => AbstractConfigModel.php} (87%) rename src/Oro/Bundle/EntityConfigBundle/Entity/{ConfigValue.php => ConfigModelValue.php} (86%) rename src/Oro/Bundle/EntityConfigBundle/Entity/{ConfigEntity.php => EntityConfigModel.php} (75%) rename src/Oro/Bundle/EntityConfigBundle/Entity/{ConfigField.php => FieldConfigModel.php} (80%) delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php rename src/Oro/Bundle/EntityConfigBundle/Event/{NewEntityEvent.php => NewConfigModelEvent.php} (52%) delete mode 100644 src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php create mode 100644 src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php diff --git a/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php index 11a76f96b75..db7843dc2f4 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php +++ b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php @@ -32,7 +32,17 @@ public function execute(InputInterface $input, OutputInterface $output) /** @var ClassMetadataInfo $doctrineMetadata */ foreach ($this->getConfigManager()->em()->getMetadataFactory()->getAllMetadata() as $doctrineMetadata) { if ($this->getConfigManager()->isConfigurable($doctrineMetadata->getName())) { - $this->getConfigManager()->createConfigEntity($doctrineMetadata->getName()); + $this->getConfigManager()->createConfigEntityModel($doctrineMetadata->getName()); + + foreach ($doctrineMetadata->getFieldNames() as $fieldName) { + $type = $doctrineMetadata->getTypeOfField($fieldName); + $this->getConfigManager()->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } + + foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { + $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; + $this->getConfigManager()->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php index 54003388064..3fd17a0a2c9 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/ConfigIdInterface.php @@ -9,6 +9,8 @@ interface ConfigIdInterface extends \Serializable */ public function getId(); + public function getEntityId(); + /** * @return string */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php index 7518d0e812a..7c53b0dc8a8 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigId.php @@ -45,6 +45,14 @@ public function getId() return 'entity_' . $this->scope . '_' . strtr($this->className, '\\', '-'); } + /** + * @return string + */ + public function getEntityId() + { + return sprintf('ConfigEntity Entity "%s"', $this->getClassName()); + } + /** * @return string */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php index 588373b2441..ac890d52324 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigId.php @@ -90,6 +90,18 @@ public function getId() return 'field_' . $this->scope . '_' . strtr($this->className, '\\', '-') . '_' . $this->fieldName; } + /** + * @return string + */ + public function getEntityId() + { + return sprintf( + 'ConfigEntity Field "%s" in Entity "%s"', + $this->getClassName(), + $this->getFieldName() + ); + } + /** * @return string */ diff --git a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php index 6a59d8d9d28..9517b3f7873 100644 --- a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php @@ -7,29 +7,31 @@ use Metadata\MetadataFactory; -use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; use Symfony\Component\EventDispatcher\EventDispatcher; +use Oro\Bundle\EntityConfigBundle\Exception\LogicException; +use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; + use Oro\Bundle\EntityConfigBundle\Audit\AuditManager; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy; use Oro\Bundle\EntityConfigBundle\Cache\CacheInterface; -use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; +use Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy; use Oro\Bundle\EntityConfigBundle\Metadata\ConfigClassMetadata; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfigModel; -use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigId; use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; + use Oro\Bundle\EntityConfigBundle\Config\Config; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; use Oro\Bundle\EntityConfigBundle\Event\PersistConfigEvent; use Oro\Bundle\EntityConfigBundle\Event\FlushConfigEvent; -use Oro\Bundle\EntityConfigBundle\Event\NewFieldEvent; -use Oro\Bundle\EntityConfigBundle\Event\NewEntityEvent; +use Oro\Bundle\EntityConfigBundle\Event\NewConfigModelEvent; use Oro\Bundle\EntityConfigBundle\Event\Events; class ConfigManager @@ -89,6 +91,11 @@ class ConfigManager */ protected $configs = array(); + /** + * @var AbstractConfigModel[] + */ + protected $entities = array(); + /** * @var ConfigInterface[] */ @@ -177,6 +184,17 @@ public function getClassMetadata($className) return $this->metadataFactory->getMetadataForClass($className); } + /** + * @return bool + */ + public function checkDatabase() + { + $tables = $this->em()->getConnection()->getSchemaManager()->listTableNames(); + $table = $this->em()->getClassMetadata(EntityConfigModel::ENTITY_NAME)->getTableName(); + + return in_array($table, $tables); + } + /** * @param $className * @return bool @@ -189,6 +207,10 @@ public function isConfigurable($className) return $metadata && $metadata->name == $className && $metadata->configurable; } + /** + * @param ConfigIdInterface $configId + * @return bool + */ public function hasConfig(ConfigIdInterface $configId) { if (isset($this->configs[$configId->getId()])) { @@ -201,12 +223,14 @@ public function hasConfig(ConfigIdInterface $configId) return true; } + return (bool) $this->getConfigModelByConfigId($configId); } /** * @param ConfigIdInterface $configId - * @return null|Config|ConfigInterface * @throws Exception\RuntimeException + * @throws Exception\LogicException + * @return null|Config|ConfigInterface */ public function getConfig(ConfigIdInterface $configId) { @@ -214,47 +238,31 @@ public function getConfig(ConfigIdInterface $configId) return $this->configs[$configId->getId()]; } + if (!$this->checkDatabase()) { + throw new LogicException( + "Database is not synced, if you use ConfigManager, when a db schema may be hasn't synced. check it by ConfigManager::checkDatabase" + ); + } + if (!$this->isConfigurable($configId->getClassName())) { throw new RuntimeException(sprintf("Entity '%s' is not Configurable", $configId->getClassName())); } - $resultConfig = null; - if (null !== $this->configCache - && $config = $this->configCache->loadConfigFromCache($configId) - ) { + if (null !== $this->configCache && $config = $this->configCache->loadConfigFromCache($configId)) { $resultConfig = $config; } else { - /** @var AbstractConfig $resultEntity */ - if (!$this->isSchemaSynced()) { //TODO::check isSchemaSynced before call ConfigManager::getConfig - $resultEntity = null; - } else { - $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); - $fieldConfigRepo = $this->em()->getRepository(ConfigField::ENTITY_NAME); - - $resultEntity = $entity = $entityConfigRepo->findOneBy(array('className' => $configId->getClassName())); - - if ($configId instanceof FieldConfigIdInterface) { - $resultEntity = $fieldConfigRepo->findOneBy( - array( - 'entity' => $resultEntity, - 'fieldName' => $configId->getFieldName() - ) - ); - } + if (!$model = $this->getConfigModelByConfigId($configId)) { + throw new RuntimeException(sprintf('%s is not found', $configId->getEntityId())); } - if ($resultEntity) { - $config = new Config($configId); - $config->setValues($resultEntity->toArray($configId->getScope())); - - if (null !== $this->configCache) { - $this->configCache->putConfigInCache($config); - } + $config = new Config($configId); + $config->setValues($model->toArray($configId->getScope())); - $resultConfig = $config; - } else { - $resultConfig = new Config($configId); + if (null !== $this->configCache) { + $this->configCache->putConfigInCache($config); } + + $resultConfig = $config; } //internal cache @@ -266,9 +274,12 @@ public function getConfig(ConfigIdInterface $configId) return $resultConfig; } - public function createConfigEntity($className) + /** + * @param $className + */ + public function createConfigEntityModel($className) { - if (!$this->em()->getRepository(ConfigEntity::ENTITY_NAME)->findOneBy(array('className' => $className))) { + if (!$this->getConfigModel($className)) { /** @var ConfigClassMetadata $metadata */ $metadata = $this->metadataFactory->getMetadataForClass($className); @@ -278,78 +289,37 @@ public function createConfigEntity($className) $defaultValues = $metadata->defaultValues[$provider->getScope()]; } - $provider->createConfig(new EntityConfigId($className, $provider->getScope()), $defaultValues); + $entityId = new EntityConfigId($className, $provider->getScope()); + $provider->createConfig($entityId, $defaultValues); - $this->eventDispatcher->dispatch(Events::NEW_ENTITY, new NewEntityEvent($className, $this)); + $this->eventDispatcher->dispatch(Events::NEW_CONFIG_MODEL, new NewConfigModelEvent($entityId, $this)); } } } /** - * @param ClassMetadataInfo $doctrineMetadata + * @param $className + * @param $fieldName + * @param $fieldType */ - public function initConfigByDoctrineMetadata(ClassMetadataInfo $doctrineMetadata) + public function createConfigFieldModel($className, $fieldName, $fieldType) { - /** @var ConfigClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($doctrineMetadata->getName()); - if ($metadata - && $metadata->name == $doctrineMetadata->getName() - && $metadata->configurable - && !$this->em()->getRepository(ConfigEntity::ENTITY_NAME)->findOneBy(array( - 'className' => $doctrineMetadata->getName())) - ) { - foreach ($this->getProviders() as $provider) { - $defaultValues = $provider->getConfigContainer()->getEntityDefaultValues(); - if (isset($metadata->defaultValues[$provider->getScope()])) { - $defaultValues = array_merge($defaultValues, $metadata->defaultValues[$provider->getScope()]); - } - - $provider->createEntityConfig($doctrineMetadata->getName(), $defaultValues); - } - - $this->eventDispatcher->dispatch( - Events::NEW_ENTITY, - new NewEntityEvent($doctrineMetadata->getName(), $this) - ); - - foreach ($doctrineMetadata->getFieldNames() as $fieldName) { - $type = $doctrineMetadata->getTypeOfField($fieldName); - - foreach ($this->getProviders() as $provider) { - $provider->createFieldConfig( - $doctrineMetadata->getName(), - $fieldName, - $type, - $provider->getConfigContainer()->getFieldDefaultValues() - ); - } - - $this->eventDispatcher->dispatch( - Events::NEW_FIELD, - new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) - ); - } + if (!$this->getConfigModel($className, $fieldName)) { + /** @var ConfigClassMetadata $metadata */ + //$metadata = $this->metadataFactory->getMetadataForClass($className); //TODO::implement default value for config - foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { - $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; + foreach ($this->getProviders() as $provider) { + $entityId = new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType); + $provider->createConfig($entityId, array()); - foreach ($this->getProviders() as $provider) { - $provider->createFieldConfig( - $doctrineMetadata->getName(), - $fieldName, - $type, - $provider->getConfigContainer()->getFieldDefaultValues() - ); - } + $provider->createConfig(new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType), array()); - $this->eventDispatcher->dispatch( - Events::NEW_FIELD, - new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) - ); + $this->eventDispatcher->dispatch(Events::NEW_CONFIG_MODEL, new NewConfigModelEvent($entityId, $this)); } } } + /** * @param ConfigIdInterface $configId */ @@ -419,7 +389,7 @@ public function flush() if ($config instanceof FieldConfigInterface) { if (!$configField = $configEntity->getField($config->getCode())) { - $configField = new ConfigField($config->getCode(), $config->getType()); + $configField = new FieldConfigModel($config->getCode(), $config->getType()); $configEntity->addField($configField); } @@ -558,43 +528,43 @@ public function getConfigChangeSet(ConfigInterface $config) } /** - * @param $className - * @return ConfigEntity + * @param ConfigIdInterface $configId + * @return AbstractConfigModel|null */ - protected function findOrCreateConfigEntity($className) + protected function getConfigModelByConfigId(ConfigIdInterface $configId) { - $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); - /** @var ConfigEntity $entity */ - $entity = $entityConfigRepo->findOneBy(array('className' => $className)); - if (!$entity) { - $metadata = $this->metadataFactory->getMetadataForClass($className); - $entity = new ConfigEntity($className); - $entity->setMode($metadata->viewMode); - - foreach ($this->getProviders() as $provider) { - $provider->createEntityConfig( - $className, - $provider->getConfigContainer()->getEntityDefaultValues() - ); - } - - $this->eventDispatcher->dispatch( - Events::NEW_ENTITY, - new NewEntityEvent($className, $this) - ); - } + $fieldName = $configId instanceof FieldConfigId ? $configId->getFieldName() : 'null'; - return $entity; + return $this->getConfigModel($configId->getClassName(), $fieldName); } /** - * @return bool + * @param $className + * @param null $fieldName + * @return object|AbstractConfigModel */ - protected function isSchemaSynced() + protected function getConfigModel($className, $fieldName = null) { - $tables = $this->em()->getConnection()->getSchemaManager()->listTableNames(); - $table = $this->em()->getClassMetadata(ConfigEntity::ENTITY_NAME)->getTableName(); + $id = $className . $fieldName; - return in_array($table, $tables); + if (isset($this->entities[$id])) { + return $this->entities[$id]; + } + + $entityConfigRepo = $this->em()->getRepository(EntityConfigModel::ENTITY_NAME); + $fieldConfigRepo = $this->em()->getRepository(FieldConfigModel::ENTITY_NAME); + + $result = $entity = $entityConfigRepo->findOneBy(array('className' => $className)); + + if ($fieldName) { + $result = $fieldConfigRepo->findOneBy( + array( + 'entity' => $result, + 'fieldName' => $fieldName + ) + ); + } + + return $result; } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php index 318d45a2a24..da4029b1b07 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Controller; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; @@ -83,7 +83,7 @@ public function auditAction($entity, $id) */ public function auditFieldAction($entity, $id) { - /** @var ConfigField $fieldName */ + /** @var FieldConfigModel $fieldName */ $fieldName = $this->getDoctrine() ->getRepository('OroEntityConfigBundle:ConfigField') ->findOneBy(array('id' => $id)); diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php index f45d1e3afb2..d39d467a019 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php @@ -17,8 +17,8 @@ use Oro\Bundle\EntityConfigBundle\Datagrid\EntityFieldsDatagridManager; use Oro\Bundle\EntityConfigBundle\Datagrid\ConfigDatagridManager; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; /** * EntityConfig controller. @@ -72,7 +72,7 @@ public function indexAction(Request $request) */ public function updateAction($id) { - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); $request = $this->getRequest(); $form = $this->createForm( @@ -124,7 +124,7 @@ public function updateAction($id) * ) * @Template() */ - public function viewAction(ConfigEntity $entity) + public function viewAction(EntityConfigModel $entity) { /** @var EntityFieldsDatagridManager $datagridManager */ $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); @@ -200,7 +200,7 @@ public function viewAction(ConfigEntity $entity) */ public function fieldsAction($id, Request $request) { - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); /** @var FieldsDatagridManager $datagridManager */ $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); @@ -241,7 +241,7 @@ public function fieldsAction($id, Request $request) */ public function fieldUpdateAction($id) { - $field = $this->getDoctrine()->getRepository(ConfigField::ENTITY_NAME)->find($id); + $field = $this->getDoctrine()->getRepository(FieldConfigModel::ENTITY_NAME)->find($id); $form = $this->createForm( 'oro_entity_config_config_field_type', diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php index ae3a27c22f5..680dd98b113 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php @@ -4,7 +4,7 @@ use Doctrine\ORM\Query; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; +use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfigModel; use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Action\ActionInterface; use Oro\Bundle\GridBundle\Datagrid\DatagridManager; @@ -313,7 +313,7 @@ protected function getRowActions() protected function prepareQuery(ProxyQueryInterface $query) { $query->where('ce.mode <> :mode'); - $query->setParameter('mode', AbstractConfig::MODE_VIEW_HIDDEN); + $query->setParameter('mode', AbstractConfigModel::MODE_VIEW_HIDDEN); foreach ($this->configManager->getProviders() as $provider) { foreach ($provider->getConfigContainer()->getEntityItems() as $code => $item) { diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php index f13954b6394..227eeb4ba00 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php @@ -13,7 +13,7 @@ use Oro\Bundle\GridBundle\Filter\FilterInterface; use Oro\Bundle\GridBundle\Property\UrlProperty; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\ConfigManager; class EntityFieldsDatagridManager extends DatagridManager @@ -24,7 +24,7 @@ class EntityFieldsDatagridManager extends DatagridManager protected $fieldsCollection; /** - * @var ConfigEntity id + * @var EntityConfigModel id */ protected $entityId; @@ -47,10 +47,10 @@ public function setEntityId($id) } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return array */ - public function getLayoutActions(ConfigEntity $entity) + public function getLayoutActions(EntityConfigModel $entity) { $actions = array(); foreach ($this->configManager->getProviders() as $provider) { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php similarity index 87% rename from src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php index f3e391927cb..ae3e056afae 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php @@ -9,7 +9,7 @@ * @ORM\MappedSuperclass * @ORM\HasLifecycleCallbacks */ -abstract class AbstractConfig +abstract class AbstractConfigModel { /** * type of config @@ -37,12 +37,12 @@ abstract class AbstractConfig protected $mode; /** - * @var ConfigValue[]|ArrayCollection + * @var ConfigModelValue[]|ArrayCollection */ protected $values; /** - * @param ConfigValue[] $values + * @param ConfigModelValue[] $values * @return $this */ public function setValues($values) @@ -57,7 +57,7 @@ public function setValues($values) } /** - * @param ConfigValue $value + * @param ConfigModelValue $value * @return $this */ public function addValue($value) @@ -88,7 +88,7 @@ public function getMode() /** * @param callable $filter - * @return array|ArrayCollection|ConfigValue[] + * @return array|ArrayCollection|ConfigModelValue[] */ public function getValues(\Closure $filter = null) { @@ -98,11 +98,11 @@ public function getValues(\Closure $filter = null) /** * @param $code * @param $scope - * @return ConfigValue + * @return ConfigModelValue */ public function getValue($code, $scope) { - $values = $this->getValues(function (ConfigValue $value) use ($code, $scope) { + $values = $this->getValues(function (ConfigModelValue $value) use ($code, $scope) { return ($value->getScope() == $scope && $value->getCode() == $code); }); @@ -169,9 +169,9 @@ public function fromArray($scope, array $values, array $serializableValues = arr $configValue->setValue($value); } else { - $configValue = new ConfigValue($code, $scope, $value, $serializable); + $configValue = new ConfigModelValue($code, $scope, $value, $serializable); - if ($this instanceof ConfigEntity) { + if ($this instanceof EntityConfigModel) { $configValue->setEntity($this); } else { $configValue->setField($this); @@ -188,7 +188,7 @@ public function fromArray($scope, array $values, array $serializableValues = arr */ public function toArray($scope) { - $values = $this->getValues(function (ConfigValue $value) use ($scope) { + $values = $this->getValues(function (ConfigModelValue $value) use ($scope) { return $value->getScope() == $scope; }); diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php similarity index 86% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php index a1c8212b4f4..e425fce2fdc 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php @@ -8,9 +8,9 @@ * @ORM\Table(name="oro_config_value") * @ORM\Entity */ -class ConfigValue +class ConfigModelValue { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigValue'; + const ENTITY_NAME = 'OroEntityConfigBundle:ConfigModelValue'; /** * @var integer @@ -21,8 +21,8 @@ class ConfigValue protected $id; /** - * @var ConfigEntity - * @ORM\ManyToOne(targetEntity="ConfigEntity", inversedBy="values") + * @var EntityConfigModel + * @ORM\ManyToOne(targetEntity="EntityConfigModel", inversedBy="values") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="entity_id", referencedColumnName="id") * }) @@ -30,8 +30,8 @@ class ConfigValue protected $entity; /** - * @var ConfigField - * @ORM\ManyToOne(targetEntity="ConfigField", inversedBy="values") + * @var FieldConfigModel + * @ORM\ManyToOne(targetEntity="FieldConfigModel", inversedBy="values") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="field_id", referencedColumnName="id") * }) @@ -83,7 +83,7 @@ public function getId() /** * Set code * @param string $code - * @return ConfigValue + * @return ConfigModelValue */ public function setCode($code) { @@ -103,7 +103,7 @@ public function getCode() /** * @param string $scope - * @return ConfigValue + * @return ConfigModelValue */ public function setScope($scope) { @@ -123,7 +123,7 @@ public function getScope() /** * Set data * @param string $value - * @return ConfigValue + * @return ConfigModelValue */ public function setValue($value) { @@ -142,7 +142,7 @@ public function getValue() } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return $this */ public function setEntity($entity) @@ -153,7 +153,7 @@ public function setEntity($entity) } /** - * @return ConfigEntity + * @return EntityConfigModel */ public function getEntity() { @@ -161,7 +161,7 @@ public function getEntity() } /** - * @param ConfigField $field + * @param FieldConfigModel $field * @return $this */ public function setField($field) @@ -172,7 +172,7 @@ public function setField($field) } /** - * @return ConfigField + * @return FieldConfigModel */ public function getField() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php b/src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php similarity index 75% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php index 37025d73132..8f981c56cf7 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php @@ -10,9 +10,9 @@ * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ -class ConfigEntity extends AbstractConfig +class EntityConfigModel extends AbstractConfigModel { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigEntity'; + const ENTITY_NAME = 'OroEntityConfigBundle:EntityConfigModel'; /** * @var integer @@ -23,14 +23,14 @@ class ConfigEntity extends AbstractConfig protected $id; /** - * @var ConfigValue[]|ArrayCollection - * @ORM\OneToMany(targetEntity="ConfigValue", mappedBy="entity", cascade={"all"}) + * @var ConfigModelValue[]|ArrayCollection + * @ORM\OneToMany(targetEntity="ConfigModelValue", mappedBy="entity", cascade={"all"}) */ protected $values; /** - * @var ConfigField[]|ArrayCollection - * @ORM\OneToMany(targetEntity="ConfigField", mappedBy="entity", cascade={"all"}) + * @var FieldConfigModel[]|ArrayCollection + * @ORM\OneToMany(targetEntity="FieldConfigModel", mappedBy="entity", cascade={"all"}) */ protected $fields; @@ -76,7 +76,7 @@ public function getClassName() } /** - * @param ConfigField[] $fields + * @param FieldConfigModel[] $fields * @return $this */ public function setFields($fields) @@ -87,7 +87,7 @@ public function setFields($fields) } /** - * @param ConfigField $field + * @param FieldConfigModel $field * @return $this */ public function addField($field) @@ -100,7 +100,7 @@ public function addField($field) /** * @param callable $filter - * @return ConfigField[]|ArrayCollection + * @return FieldConfigModel[]|ArrayCollection */ public function getFields(\Closure $filter = null) { @@ -109,11 +109,11 @@ public function getFields(\Closure $filter = null) /** * @param $fieldName - * @return ConfigField + * @return FieldConfigModel */ public function getField($fieldName) { - $fields = $this->getFields(function (ConfigField $field) use ($fieldName) { + $fields = $this->getFields(function (FieldConfigModel $field) use ($fieldName) { return $field->getFieldName() == $fieldName; }); diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php b/src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php similarity index 80% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php index ab39ee95387..53db1f68618 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php @@ -6,16 +6,16 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; +use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfigModel; /** * @ORM\Table(name="oro_config_field") * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ -class ConfigField extends AbstractConfig +class FieldConfigModel extends AbstractConfigModel { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigField'; + const ENTITY_NAME = 'OroEntityConfigBundle:FieldConfigModel'; /** * @var integer @@ -26,8 +26,8 @@ class ConfigField extends AbstractConfig protected $id; /** - * @var ConfigEntity - * @ORM\ManyToOne(targetEntity="ConfigEntity", inversedBy="fields") + * @var EntityConfigModel + * @ORM\ManyToOne(targetEntity="EntityConfigModel", inversedBy="fields") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="entity_id", referencedColumnName="id") * }) @@ -35,8 +35,8 @@ class ConfigField extends AbstractConfig protected $entity; /** - * @var ConfigValue[]|PersistentCollection - * @ORM\OneToMany(targetEntity="ConfigValue", mappedBy="field", cascade={"all"}) + * @var ConfigModelValue[]|PersistentCollection + * @ORM\OneToMany(targetEntity="ConfigModelValue", mappedBy="field", cascade={"all"}) */ protected $values; @@ -107,7 +107,7 @@ public function getType() } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return $this */ public function setEntity($entity) @@ -118,7 +118,7 @@ public function setEntity($entity) } /** - * @return ConfigEntity + * @return EntityConfigModel */ public function getEntity() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/Events.php b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php index 3d159cdf4fe..9eea8030b04 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Event/Events.php +++ b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php @@ -7,10 +7,8 @@ final class Events /** * Config Event Names */ - const NEW_ENTITY = 'entity_config.new.entity'; - const NEW_FIELD = 'entity_config.new.field'; - const PERSIST_CONFIG = 'entity_config.persist.config'; - const PRE_FLUSH = 'entity_config.pre.flush'; - const ON_FLUSH = 'entity_config.on.flush'; - const POST_FLUSH = 'entity_config.post.flush'; + const NEW_CONFIG_MODEL = 'entity_config.new.config_model'; + const PERSIST_CONFIG = 'entity_config.persist.config'; + const UPDATE_CONFIG = 'entity_config.update.config'; + const REMOVE_CONFIG = 'entity_config.remove.config'; } diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php deleted file mode 100644 index 444ea656e46..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php +++ /dev/null @@ -1,31 +0,0 @@ -configManager = $configManager; - } - - /** - * @return ConfigManager - */ - public function getConfigManager() - { - return $this->configManager; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewConfigModelEvent.php similarity index 52% rename from src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php rename to src/Oro/Bundle/EntityConfigBundle/Event/NewConfigModelEvent.php index 62c8bb99fba..7ae29adae9f 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php +++ b/src/Oro/Bundle/EntityConfigBundle/Event/NewConfigModelEvent.php @@ -4,15 +4,15 @@ use Symfony\Component\EventDispatcher\Event; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; use Oro\Bundle\EntityConfigBundle\ConfigManager; -class NewEntityEvent extends Event +class NewConfigModelEvent extends Event { /** * @var string */ - protected $className; + protected $configId; /** * @var ConfigManager @@ -20,21 +20,21 @@ class NewEntityEvent extends Event protected $configManager; /** - * @param string $className - * @param ConfigManager $configManager + * @param ConfigIdInterface $configId + * @param ConfigManager $configManager */ - public function __construct($className, ConfigManager $configManager) + public function __construct(ConfigIdInterface $configId, ConfigManager $configManager) { - $this->className = $className; + $this->configId = $configId; $this->configManager = $configManager; } /** - * @return EntityConfig + * @return ConfigIdInterface */ - public function getClassName() + public function getConfigId() { - return $this->className; + return $this->configId; } /** diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php deleted file mode 100644 index ed319606f60..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php +++ /dev/null @@ -1,77 +0,0 @@ -className = $className; - $this->fieldName = $fieldName; - $this->fieldType = $fieldType; - $this->configManager = $configManager; - } - - /** - * @return EntityConfig - */ - public function getClassName() - { - return $this->className; - } - - /** - * @return string - */ - public function getFieldName() - { - return $this->fieldName; - } - - /** - * @return string - */ - public function getFieldType() - { - return $this->fieldType; - } - - /** - * @return ConfigManager - */ - public function getConfigManager() - { - return $this->configManager; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php b/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php new file mode 100644 index 00000000000..a72b857bc0d --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php @@ -0,0 +1,7 @@ +viewMode, array(AbstractConfig::MODE_VIEW_DEFAULT, AbstractConfig::MODE_VIEW_HIDDEN, AbstractConfig::MODE_VIEW_READONLY))) { + if (!in_array($this->viewMode, array(AbstractConfigModel::MODE_VIEW_DEFAULT, AbstractConfigModel::MODE_VIEW_HIDDEN, AbstractConfigModel::MODE_VIEW_READONLY))) { throw new AnnotationException(sprintf('Annotation "Configurable" give invalid parameter "viewMode" : "%s"', $this->viewMode)); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php index 4a7776d7b29..5e8515a06e4 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php @@ -13,7 +13,7 @@ use Oro\Bundle\EntityConfigBundle\ConfigManager; use Oro\Bundle\EntityConfigBundle\DependencyInjection\EntityConfigContainer; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\Metadata\Driver\AnnotationDriver; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; use Oro\Bundle\EntityConfigBundle\Tests\Unit\Fixture\Repository\NotFoundEntityConfigRepository; @@ -89,14 +89,14 @@ public function setUp() public function testGetConfigFoundConfigEntity() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); } public function testGetConfigNotFoundConfigEntity() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); } @@ -111,7 +111,7 @@ public function testGetConfigNotFoundCache() { $this->configCache->expects($this->any())->method('loadConfigFromCache')->will($this->returnValue(null)); - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); $this->configManager->setCache($this->configCache); @@ -139,7 +139,7 @@ public function testGetEventDispatcher() public function testClearCache() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); @@ -163,7 +163,7 @@ public function testAddAndGetProvider() public function testInitConfigByDoctrineMetadata() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); $this->configManager->addProvider($this->provider); @@ -189,7 +189,7 @@ public function testRemove() public function testFlush() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') ->disableOriginalConstructor() @@ -217,7 +217,7 @@ public function testFlush() public function testChangeSet() { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); + $meta = $this->em->getClassMetadata(EntityConfigModel::ENTITY_NAME); $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); $config = $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); $configField = $config->getFields()->first(); diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php index 2e0a2c18683..945219f8d87 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -2,26 +2,26 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Entity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigValue; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\ConfigModelValue; class ConfigTest extends \PHPUnit_Framework_TestCase { - /** @var ConfigEntity */ + /** @var EntityConfigModel */ private $configEntity; - /** @var ConfigField */ + /** @var FieldConfigModel */ private $configField; - /** @var ConfigValue */ + /** @var ConfigModelValue */ private $configValue; protected function setUp() { - $this->configEntity = new ConfigEntity(); - $this->configField = new ConfigField(); - $this->configValue = new ConfigValue(); + $this->configEntity = new EntityConfigModel(); + $this->configField = new FieldConfigModel(); + $this->configValue = new ConfigModelValue(); } public function testProperties() @@ -51,8 +51,8 @@ public function testProperties() /** test ConfigField */ $this->assertEmpty($this->configField->getId()); - $this->configField->setMode(ConfigField::MODE_VIEW_READONLY); - $this->assertEquals(ConfigField::MODE_VIEW_READONLY, $this->configField->getMode()); + $this->configField->setMode(FieldConfigModel::MODE_VIEW_READONLY); + $this->assertEquals(FieldConfigModel::MODE_VIEW_READONLY, $this->configField->getMode()); /** test ConfigValue */ $this->assertEmpty($this->configValue->getId()); @@ -157,7 +157,7 @@ public function testToFromArray() 'doctrine' => true ); - $this->configField->addValue(new ConfigValue('is_searchable', 'datagrid', false)); + $this->configField->addValue(new ConfigModelValue('is_searchable', 'datagrid', false)); $this->configField->fromArray('datagrid', $values, $serializable); $this->assertEquals( array( @@ -168,7 +168,7 @@ public function testToFromArray() $this->configField->toArray('datagrid') ); - $this->configEntity->addValue(new ConfigValue('is_searchable', 'datagrid', false)); + $this->configEntity->addValue(new ConfigModelValue('is_searchable', 'datagrid', false)); $this->configEntity->fromArray('datagrid', $values, $serializable); $this->assertEquals( array( diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php index 637f6a98b93..e9ac299b4a6 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php @@ -3,7 +3,7 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Event; use Oro\Bundle\EntityConfigBundle\ConfigManager; -use Oro\Bundle\EntityConfigBundle\Event\NewEntityEvent; +use Oro\Bundle\EntityConfigBundle\Event\NewConfigModelEvent; use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; class EntityConfigEventTest extends \PHPUnit_Framework_TestCase @@ -26,9 +26,9 @@ protected function setUp() public function testEvent() { - $event = new NewEntityEvent(ConfigManagerTest::DEMO_ENTITY, $this->configManager); + $event = new NewConfigModelEvent(ConfigManagerTest::DEMO_ENTITY, $this->configManager); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getClassName()); + $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getConfigId()); $this->assertEquals($this->configManager, $event->getConfigManager()); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php index fc351acf604..6c88376b1bb 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php @@ -4,9 +4,9 @@ use Doctrine\ORM\EntityRepository; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigValue; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\ConfigModelValue; use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; class FoundEntityConfigRepository extends EntityRepository @@ -21,13 +21,13 @@ public function findOneBy(array $criteria, array $orderBy = null) public static function getResultConfigEntity() { if (!self::$configEntity) { - self::$configEntity = new ConfigEntity(ConfigManagerTest::DEMO_ENTITY); + self::$configEntity = new EntityConfigModel(ConfigManagerTest::DEMO_ENTITY); - $configField = new ConfigField('test', 'string'); + $configField = new FieldConfigModel('test', 'string'); self::$configEntity->addField($configField); - $configValue = new ConfigValue('test_value', 'test', 'test_value_origin'); - $configValueSerializable = new ConfigValue('test_value_serializable', 'test', array('test_value' => 'test_value_origin')); + $configValue = new ConfigModelValue('test_value', 'test', 'test_value_origin'); + $configValueSerializable = new ConfigModelValue('test_value_serializable', 'test', array('test_value' => 'test_value_origin')); self::$configEntity->addValue($configValue); self::$configEntity->addValue($configValueSerializable); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php index 6d6b757b50c..5f0c8d2fc29 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Metadata\Annotation; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; +use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfigModel; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; class ConfigurableTest extends \PHPUnit_Framework_TestCase @@ -13,7 +13,7 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase public function testTrue(array $data) { $annot = new Configurable($data); - $this->assertEquals(AbstractConfig::MODE_VIEW_HIDDEN, $annot->viewMode); + $this->assertEquals(AbstractConfigModel::MODE_VIEW_HIDDEN, $annot->viewMode); $this->assertEquals('symfony_route_name', $annot->routeName); $this->assertEquals(array('key' => 'value'), $annot->defaultValues); } @@ -33,14 +33,14 @@ public function providerTrue() return array( array( array( - 'value' => AbstractConfig::MODE_VIEW_HIDDEN, + 'value' => AbstractConfigModel::MODE_VIEW_HIDDEN, 'routeName' => 'symfony_route_name', 'defaultValues' => array('key' => 'value'), ), ), array( array( - 'viewMode' => AbstractConfig::MODE_VIEW_HIDDEN, + 'viewMode' => AbstractConfigModel::MODE_VIEW_HIDDEN, 'routeName' => 'symfony_route_name', 'defaultValues' => array('key' => 'value'), ), @@ -62,7 +62,7 @@ public function providerFalse() ), array( array( - 'viewMode' => AbstractConfig::MODE_VIEW_HIDDEN, + 'viewMode' => AbstractConfigModel::MODE_VIEW_HIDDEN, 'routeName' => 'symfony_route_name', 'defaultValues' => 'wrong_value', ), diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php index d6361ea7aa7..dbfffeaacde 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php +++ b/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php @@ -4,7 +4,7 @@ use Doctrine\ORM\EntityManager; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; @@ -44,8 +44,8 @@ public function execute(InputInterface $input, OutputInterface $output) /** @var ExtendManager $xm */ $xm = $this->getContainer()->get('oro_entity_extend.extend.extend_manager'); - /** @var ConfigEntity[] $configs */ - $configs = $em->getRepository(ConfigEntity::ENTITY_NAME)->findAll(); + /** @var EntityConfigModel[] $configs */ + $configs = $em->getRepository(EntityConfigModel::ENTITY_NAME)->findAll(); foreach ($configs as $config) { if ($xm->isExtend($config->getClassName())) { $xm->getClassGenerator()->checkEntityCache($config->getClassName(), true); diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php index fa7c297f70e..003278b2359 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php @@ -11,7 +11,7 @@ use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityExtendBundle\Tools\Schema; use Symfony\Component\Process\Process; @@ -44,8 +44,8 @@ class ApplyController extends Controller */ public function applyAction($id) { - /** @var ConfigEntity $entity */ - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + /** @var EntityConfigModel $entity */ + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); /** @var ConfigProvider $entityConfigProvider */ $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); @@ -120,8 +120,8 @@ public function applyAction($id) */ public function updateAction($id) { - /** @var ConfigEntity $entity */ - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + /** @var EntityConfigModel $entity */ + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); $env = $this->get('kernel')->getEnvironment(); $commands = array( diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php index 3a956c4b018..b16eb0f41b0 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php @@ -10,7 +10,7 @@ use Oro\Bundle\UserBundle\Annotation\Acl; use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; use Oro\Bundle\EntityExtendBundle\Form\Type\UniqueKeyCollectionType; @@ -42,7 +42,7 @@ class ConfigEntityGridController extends Controller * ) * @Template */ - public function uniqueAction(ConfigEntity $entity) + public function uniqueAction(EntityConfigModel $entity) { /** @var ConfigProvider $configProvider */ $configProvider = $this->get('oro_entity_extend.config.extend_config_provider'); diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php index 60fc92ed9b5..437e303ca90 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php @@ -15,8 +15,8 @@ use Oro\Bundle\UserBundle\Annotation\Acl; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; use Oro\Bundle\EntityExtendBundle\Form\Type\FieldType; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\ConfigManager; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; @@ -42,7 +42,7 @@ class ConfigFieldGridController extends Controller * ) * @Template */ - public function createAction(ConfigEntity $entity) + public function createAction(EntityConfigModel $entity) { /** @var ExtendManager $extendManager */ $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); @@ -114,7 +114,7 @@ public function createAction(ConfigEntity $entity) * parent="oro_entityextend" * ) */ - public function removeAction(ConfigField $field) + public function removeAction(FieldConfigModel $field) { if (!$field) { throw $this->createNotFoundException('Unable to find ConfigField entity.'); diff --git a/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php b/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php index 23ecfe4130a..c4dc3f9252e 100644 --- a/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php +++ b/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php @@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Oro\Bundle\EntityConfigBundle\Event\PersistConfigEvent; -use Oro\Bundle\EntityConfigBundle\Event\NewEntityEvent; +use Oro\Bundle\EntityConfigBundle\Event\NewConfigModelEvent; use Oro\Bundle\EntityConfigBundle\Event\Events; use Oro\Bundle\EntityExtendBundle\Metadata\ExtendClassMetadata; @@ -43,24 +43,24 @@ public function __construct(ExtendManager $extendManager, MetadataFactory $metad public static function getSubscribedEvents() { return array( - Events::NEW_ENTITY => 'newEntityConfig', + Events::NEW_CONFIG_MODEL => 'newEntityConfig', Events::PERSIST_CONFIG => 'persistConfig', ); } /** - * @param NewEntityEvent $event + * @param NewConfigModelEvent $event */ - public function newEntityConfig(NewEntityEvent $event) + public function newEntityConfig(NewConfigModelEvent $event) { /** @var ExtendClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($event->getClassName()); + $metadata = $this->metadataFactory->getMetadataForClass($event->getConfigId()); if ($metadata && $metadata->isExtend) { - $extendClass = $this->extendManager->getClassGenerator()->generateExtendClassName($event->getClassName()); - $proxyClass = $this->extendManager->getClassGenerator()->generateProxyClassName($event->getClassName()); + $extendClass = $this->extendManager->getClassGenerator()->generateExtendClassName($event->getConfigId()); + $proxyClass = $this->extendManager->getClassGenerator()->generateProxyClassName($event->getConfigId()); $this->extendManager->getConfigProvider()->createEntityConfig( - $event->getClassName(), + $event->getConfigId(), $values = array( 'is_extend' => true, 'extend_class' => $extendClass, diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php index 1f5d6eb1ff4..664c2557cf1 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php @@ -6,7 +6,7 @@ use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\AbstractQuery; use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; class Schema From b4c01604563974bc379c9da745993832f2ccf4d8 Mon Sep 17 00:00:00 2001 From: Roman Grebenchuk Date: Fri, 2 Aug 2013 17:39:48 +0300 Subject: [PATCH 063/541] BAP-1358: Add group Roles REST API test - added get group roles test - minor refactoring --- .../Functional/API/RestApiGroupsTest.php | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Oro/Bundle/UserBundle/Tests/Functional/API/RestApiGroupsTest.php b/src/Oro/Bundle/UserBundle/Tests/Functional/API/RestApiGroupsTest.php index 385f66c3bc3..1a7651ce4ec 100644 --- a/src/Oro/Bundle/UserBundle/Tests/Functional/API/RestApiGroupsTest.php +++ b/src/Oro/Bundle/UserBundle/Tests/Functional/API/RestApiGroupsTest.php @@ -55,14 +55,30 @@ public function testApiGetGroups($request) $this->client->request('GET', $this->client->generate('oro_api_get_groups')); $result = $this->client->getResponse(); $result = json_decode($result->getContent(), true); - $flag = 1; foreach ($result as $group) { if ($group['name'] == $request['group']['name']) { - $flag = 0; + //TODO Change after BAP-1357 fix + $this->assertEquals($request['group']['name'], $group['name']); break; } } - $this->assertEquals(0, $flag); + $this->assertNotEquals(0, $this->getCount(), 'Created group is not in groups list'); + + return $group; + } + + /** + * @depends testApiCreateGroup + * @depends testApiGetGroups + * @param array $request + * @param array $group + * @return array $group + */ + public function testApiRolesGroup($request, $group) + { + $this->client->request('GET', $this->client->generate('oro_api_get_group_roles', array('id' => $group['id']))); + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200); return $group; } From 9deb182745402764926a207be8381b46b903baca Mon Sep 17 00:00:00 2001 From: Ignat Shcheglovskyi Date: Fri, 2 Aug 2013 17:59:34 +0300 Subject: [PATCH 064/541] CRM-323: Make Address entity plain - removed dependency on OroFlexibleEntityBundle from OroAddressBundle - made code style fixes - refactored of entities and form types - updated docs - updated tests - updated views --- .../AttributeType/AddressType.php | 15 --- .../Controller/Api/Rest/AddressController.php | 14 +- .../Controller/Api/Soap/AddressController.php | 12 +- .../AddressBundle/Entity/AbstractAddress.php | 69 +++++++--- .../Entity/AbstractTypedAddress.php | 3 +- .../Bundle/AddressBundle/Entity/Address.php | 14 -- .../AddressBundle/Entity/AddressSoap.php | 19 --- .../Bundle/AddressBundle/Entity/Country.php | 3 - .../Entity/Manager/AddressManager.php | 72 ++-------- .../Bundle/AddressBundle/Entity/Region.php | 2 - .../Entity/Repository/AddressRepository.php | 8 +- .../Repository/AddressTypeRepository.php | 4 - .../Entity/Repository/RegionRepository.php | 3 - .../Entity/Value/AddressValue.php | 45 ------- .../Form/Handler/AddressHandler.php | 12 +- .../Form/Type/AbstractAddressType.php | 78 ----------- .../Form/Type/AbstractTypedAddressType.php | 63 ++++++--- .../Form/Type/AddressApiType.php | 22 +-- .../Form/Type/AddressApiValueType.php | 16 --- .../AddressBundle/Form/Type/AddressType.php | 56 +++++++- .../Form/Type/AddressValueType.php | 16 --- .../Resources/config/flexibleentity.yml | 6 - .../Resources/config/form_types.yml | 24 ---- .../Resources/config/services.yml | 43 +----- .../Resources/doc/reference/entities.md | 1 - .../Resources/doc/reference/form_types.md | 13 +- .../Resources/doc/reference/installation.md | 1 - .../Resources/doc/reference/usage.md | 2 +- .../views/Include/viewMacro.html.twig | 12 +- .../Tests/Unit/Entity/AbstractAddressTest.php | 47 ++----- .../Tests/Unit/Entity/AddressTypeTest.php | 36 ----- .../Entity/Manager/AddressManagerTest.php | 65 +-------- .../Form/Type/AbstractAddressTypeTest.php | 127 ------------------ .../Type/AbstractTypedAddressTypeTest.php | 50 +++---- .../Unit/Form/Type/AddressApiTypeTest.php | 36 ++--- .../Form/Type/AddressCollectionTypeTest.php | 3 - .../Tests/Unit/Form/Type/AddressTypeTest.php | 94 +++++++++++-- .../Unit/Twig/HasAddressExtensionTest.php | 112 --------------- .../Twig/HasAddressExtension.php | 55 -------- src/Oro/Bundle/AddressBundle/composer.json | 1 - 40 files changed, 340 insertions(+), 934 deletions(-) delete mode 100644 src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php delete mode 100644 src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php delete mode 100644 src/Oro/Bundle/AddressBundle/Entity/Value/AddressValue.php delete mode 100644 src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php delete mode 100644 src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php delete mode 100644 src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php delete mode 100644 src/Oro/Bundle/AddressBundle/Resources/config/flexibleentity.yml delete mode 100644 src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php delete mode 100644 src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php delete mode 100644 src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php delete mode 100644 src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php diff --git a/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php b/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php deleted file mode 100644 index 6df0ed20650..00000000000 --- a/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php +++ /dev/null @@ -1,15 +0,0 @@ -id; } + /** + * Set id + * + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + /** * Set label * @@ -145,7 +168,7 @@ public function getLabel() /** * Set street * - * @param string $street + * @param string $street * @return AbstractAddress */ public function setStreet($street) @@ -168,7 +191,7 @@ public function getStreet() /** * Set street2 * - * @param string $street2 + * @param string $street2 * @return AbstractAddress */ public function setStreet2($street2) @@ -191,7 +214,7 @@ public function getStreet2() /** * Set city * - * @param string $city + * @param string $city * @return AbstractAddress */ public function setCity($city) @@ -274,7 +297,7 @@ public function getUniversalState() /** * Set postal_code * - * @param string $postalCode + * @param string $postalCode * @return AbstractAddress */ public function setPostalCode($postalCode) @@ -297,7 +320,7 @@ public function getPostalCode() /** * Set country * - * @param Country $country + * @param Country $country * @return AbstractAddress */ public function setCountry($country) @@ -374,6 +397,16 @@ public function getCreatedAt() return $this->created; } + /** + * Set address created date/time + * + * @param \DateTime $created + */ + public function setCreatedAt(\DateTime $created) + { + $this->created = $created; + } + /** * Get address last update date/time * @@ -384,6 +417,16 @@ public function getUpdatedAt() return $this->updated; } + /** + * Set address updated date/time + * + * @param \DateTime $updated + */ + public function setUpdatedAt(\DateTime $updated) + { + $this->updated = $updated; + } + /** * Pre persist event listener * @@ -440,7 +483,7 @@ public function __toString() */ public function isEmpty() { - $isEmpty = empty($this->label) + return empty($this->label) && empty($this->firstName) && empty($this->lastName) && empty($this->street) @@ -450,11 +493,5 @@ public function isEmpty() && empty($this->stateText) && empty($this->country) && empty($this->postalCode); - /** @var FlexibleValueInterface $value */ - foreach ($this->values as $value) { - $flexibleValue = $value->getData(); - $isEmpty = $isEmpty && empty($flexibleValue); - } - return $isEmpty; } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php index 925ba3d8da3..ca4e10194b5 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php @@ -21,6 +21,7 @@ abstract class AbstractTypedAddress extends AbstractAddress * Many-to-many relation field, relation parameters must be in specific class * * @var Collection + * * @Soap\ComplexType("Oro\Bundle\AddressBundle\Entity\AddressType[]", nillable=true) */ protected $types; @@ -35,8 +36,6 @@ abstract class AbstractTypedAddress extends AbstractAddress public function __construct() { - parent::__construct(); - $this->types = new ArrayCollection(); } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Address.php b/src/Oro/Bundle/AddressBundle/Entity/Address.php index cb87b3b2246..d56100036d6 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Address.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Address.php @@ -3,27 +3,13 @@ namespace Oro\Bundle\AddressBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use JMS\Serializer\Annotation\Exclude; /** * Address * * @ORM\Table("oro_address") - * @ORM\HasLifecycleCallbacks() * @ORM\Entity(repositoryClass="Oro\Bundle\AddressBundle\Entity\Repository\AddressRepository") */ class Address extends AbstractAddress { - /** - * This inheritance needed to add possibility to store address in separate table - * http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html - */ - - /** - * @var \Oro\Bundle\FlexibleEntityBundle\Model\AbstractFlexibleValue[] - * - * @ORM\OneToMany(targetEntity="Oro\Bundle\AddressBundle\Entity\Value\AddressValue", mappedBy="entity", cascade={"persist", "remove"}, orphanRemoval=true) - * @Exclude - */ - protected $values; } diff --git a/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php b/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php deleted file mode 100644 index a8a9c8d1ac7..00000000000 --- a/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php +++ /dev/null @@ -1,19 +0,0 @@ -getClassMetadata($class); $this->class = $metadata->getName(); $this->om = $om; - $this->fm = $fm; } /** * Returns an empty address instance * - * @return \Oro\Bundle\AddressBundle\Entity\AbstractAddress + * @return AbstractAddress */ public function createAddress() { @@ -64,8 +48,8 @@ public function createAddress() /** * Updates an address * - * @param AbstractAddress $address - * @param bool $flush Whether to flush the changes (default true) + * @param AbstractAddress $address + * @param bool $flush Whether to flush the changes (default true) * @throws \RuntimeException */ public function updateAddress(AbstractAddress $address, $flush = true) @@ -90,7 +74,7 @@ public function deleteAddress(AbstractAddress $address) /** * Finds one address by the given criteria * - * @param array $criteria + * @param array $criteria * @return AbstractAddress */ public function findAddressBy(array $criteria) @@ -121,7 +105,7 @@ public function getClass() /** * Return related repository * - * @return \Doctrine\Common\Persistence\ObjectRepository + * @return ObjectRepository */ public function getRepository() { @@ -131,42 +115,10 @@ public function getRepository() /** * Retrieve object manager * - * @return \Doctrine\Common\Persistence\ObjectManager + * @return ObjectManager */ public function getStorageManager() { return $this->om; } - - /** - * Returns basic query instance to get collection with all user instances - * - * @param int $limit - * @param int $offset - * @return Paginator - */ - public function getListQuery($limit = 10, $offset = 1) - { - /** @var FlexibleEntityRepository $repository */ - $repository = $this->fm->getFlexibleRepository(); - - return $repository->findByWithAttributesQB(array(), null, array('id' => 'ASC'), $limit, $offset); - } - - /** - * Provide proxy method calls to flexible manager - * - * @param string $name - * @param array $args - * @return mixed - * @throws \RuntimeException - */ - public function __call($name, $args) - { - if (method_exists($this->fm, $name)) { - return call_user_func_array(array($this->fm, $name), $args); - } - - throw new \RuntimeException(sprintf('Unknown method "%s"', $name)); - } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Region.php b/src/Oro/Bundle/AddressBundle/Entity/Region.php index 8a5edd5dd89..6e4246201c9 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Region.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Region.php @@ -4,7 +4,6 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; -use JMS\Serializer\Annotation\Type; use BeSimple\SoapBundle\ServiceDefinition\Annotation as Soap; use Gedmo\Mapping\Annotation as Gedmo; use Gedmo\Translatable\Translatable; @@ -36,7 +35,6 @@ class Region implements Translatable * * @ORM\ManyToOne(targetEntity="Country", inversedBy="regions", cascade={"persist"}) * @ORM\JoinColumn(name="country_code", referencedColumnName="iso2_code") - * @Type("string") * @Soap\ComplexType("string", nillable=true) */ protected $country; diff --git a/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressRepository.php b/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressRepository.php index 531f7309e1c..69a95c1cfa8 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressRepository.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressRepository.php @@ -2,12 +2,8 @@ namespace Oro\Bundle\AddressBundle\Entity\Repository; -use Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository; +use Doctrine\ORM\EntityRepository; -/** - * Class AddressRepository - * @package Oro\Bundle\AddressBundle\Entity\Repository - */ -class AddressRepository extends FlexibleEntityRepository +class AddressRepository extends EntityRepository { } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressTypeRepository.php b/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressTypeRepository.php index 7a733333ff6..55e1c4ce68a 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressTypeRepository.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Repository/AddressTypeRepository.php @@ -4,10 +4,6 @@ use Doctrine\ORM\EntityRepository; -/** - * Class AddressTypeRepository - * @package Oro\Bundle\AddressBundle\Entity\Repository - */ class AddressTypeRepository extends EntityRepository { } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Repository/RegionRepository.php b/src/Oro/Bundle/AddressBundle/Entity/Repository/RegionRepository.php index 17013073e96..f099dac10f1 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Repository/RegionRepository.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Repository/RegionRepository.php @@ -9,9 +9,6 @@ use Oro\Bundle\AddressBundle\Entity\Country; use Oro\Bundle\AddressBundle\Entity\Region; -/** - * @package Oro\Bundle\AddressBundle\Entity\Repository - */ class RegionRepository extends EntityRepository { /** diff --git a/src/Oro/Bundle/AddressBundle/Entity/Value/AddressValue.php b/src/Oro/Bundle/AddressBundle/Entity/Value/AddressValue.php deleted file mode 100644 index 88a76791049..00000000000 --- a/src/Oro/Bundle/AddressBundle/Entity/Value/AddressValue.php +++ /dev/null @@ -1,45 +0,0 @@ -form->setData($entity); @@ -64,9 +64,9 @@ public function process(AbstractAddress $entity) /** * "Success" form handler * - * @param AbstractAddress $entity + * @param Address $entity */ - protected function onSuccess(AbstractAddress $entity) + protected function onSuccess(Address $entity) { $this->manager->persist($entity); $this->manager->flush(); diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php deleted file mode 100644 index 17f7067b303..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php +++ /dev/null @@ -1,78 +0,0 @@ -eventListener = $eventListener; - parent::__construct($flexibleManager, $valueFormAlias); - } - - /** - * {@inheritdoc} - */ - public function addEntityFields(FormBuilderInterface $builder) - { - // add default flexible fields - parent::addEntityFields($builder); - - $builder->addEventSubscriber($this->eventListener); - - // address fields - $builder - ->add('label', 'text', array('required' => false, 'label' => 'Label')) - ->add('firstName', 'text', array('required' => false, 'label' => 'First Name')) - ->add('lastName', 'text', array('required' => false, 'label' => 'Last Name')) - ->add('street', 'text', array('required' => true, 'label' => 'Street')) - ->add('street2', 'text', array('required' => false, 'label' => 'Street 2')) - ->add('city', 'text', array('required' => true, 'label' => 'City')) - ->add('state', 'oro_region', array('required' => false, 'label' => 'State')) - ->add('state_text', 'hidden', array('required' => false, 'label' => 'Custom State')) - ->add('country', 'oro_country', array('required' => true, 'label' => 'Country')) - ->add('postalCode', 'text', array('required' => true, 'label' => 'ZIP/Postal code')); - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults( - array( - 'data_class' => $this->flexibleClass, - 'intention' => 'address', - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', - ) - ); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - throw new \BadMethodCallException(sprintf('Method %s::getName() must be overridden.', get_called_class())); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php index 61f4e3b4ce9..331b39ee5b9 100644 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php +++ b/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php @@ -2,36 +2,63 @@ namespace Oro\Bundle\AddressBundle\Form\Type; +use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; -abstract class AbstractTypedAddressType extends AbstractAddressType +abstract class AbstractTypedAddressType extends AbstractType { /** * {@inheritdoc} */ - public function addEntityFields(FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add( - 'types', - 'translatable_entity', - array( - 'class' => 'OroAddressBundle:AddressType', - 'property' => 'label', - 'required' => false, - 'multiple' => true, - 'expanded' => true, + $builder + ->add( + 'types', + 'translatable_entity', + array( + 'class' => 'OroAddressBundle:AddressType', + 'property' => 'label', + 'required' => false, + 'multiple' => true, + 'expanded' => true, + ) ) - ); + ->add( + 'primary', + 'checkbox', + array( + 'label' => 'Primary', + 'required' => false + ) + ); + } - $builder->add( - 'primary', - 'checkbox', + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( array( - 'label' => 'Primary', - 'required' => false + 'data_class' => $this->getDataClass() ) ); + } - parent::addEntityFields($builder); + /** + * Get value for option "data_class" + * + * @return string + */ + abstract protected function getDataClass(); + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_address'; } } diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php index 3b8ecc60b33..e9b6bc7f4e9 100644 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php +++ b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php @@ -2,18 +2,19 @@ namespace Oro\Bundle\AddressBundle\Form\Type; -use Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Form\AbstractType; + +use Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber; -class AddressApiType extends AddressType +class AddressApiType extends AbstractType { /** * {@inheritdoc} */ - public function addEntityFields(FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder, array $options) { - parent::addEntityFields($builder); $builder->addEventSubscriber(new PatchSubscriber()); } @@ -24,14 +25,19 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( array( - 'data_class' => $this->flexibleClass, - 'intention' => 'address', - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', - 'csrf_protection' => false, + 'csrf_protection' => false, ) ); } + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_address'; + } + /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php deleted file mode 100644 index ab0271676f3..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php +++ /dev/null @@ -1,16 +0,0 @@ -eventListener = $eventListener; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->eventListener); + + $builder + ->add('id', 'hidden') + ->add('label', 'text', array('required' => false, 'label' => 'Label')) + ->add('firstName', 'text', array('required' => false, 'label' => 'First Name')) + ->add('lastName', 'text', array('required' => false, 'label' => 'Last Name')) + ->add('street', 'text', array('required' => true, 'label' => 'Street')) + ->add('street2', 'text', array('required' => false, 'label' => 'Street 2')) + ->add('city', 'text', array('required' => true, 'label' => 'City')) + ->add('state', 'oro_region', array('required' => false, 'label' => 'State')) + ->add('state_text', 'hidden', array('required' => false, 'label' => 'Custom State')) + ->add('country', 'oro_country', array('required' => true, 'label' => 'Country')) + ->add('postalCode', 'text', array('required' => true, 'label' => 'ZIP/Postal code')); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\AddressBundle\Entity\Address', + 'intention' => 'address', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + ) + ); + } + /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php deleted file mode 100644 index ee155c4328d..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php +++ /dev/null @@ -1,16 +0,0 @@ -get('oro_address.address.provider')->getStorage(); //create empty address entity - $address = $addressManager->createFlexible(); + $address = $addressManager->createAddress(); //process insert/update $this->get('oro_address.form.handler.address')->process($entity) diff --git a/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig b/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig index 1513fd46edf..156fc302be5 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig +++ b/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig @@ -16,20 +16,10 @@ {{ __.renderAddressView(address, fields) }} {% endmacro %} -{% macro renderAddressView(address, fields, renderFlexibleAttributes) %} - {# Render flexible attributes by default #} - {% set renderFlexibleAttributes = renderFlexibleAttributes|default(true) %} - - {# Render static attributes #} +{% macro renderAddressView(address, fields) %} {% for field in fields %} {{ block('addressStaticAttribute') }} {% endfor %} - - {# Render flexible attributes #} - {% import 'OroUIBundle::macros.html.twig' as UI %} - {% for value in address|getAttributes() %} - {{ UI.flexibleAttributeRow(value) }} - {% endfor %} {% endmacro %} {% block addressStaticAttribute %} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php index d7155df5e3d..a4ae63189b9 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php @@ -13,7 +13,7 @@ class AbstractAddressTest extends \PHPUnit_Framework_TestCase */ public function testSettersAndGetters($property, $value) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); call_user_func_array(array($obj, 'set' . ucfirst($property)), array($value)); $this->assertEquals($value, call_user_func_array(array($obj, 'get' . ucfirst($property)), array())); @@ -44,14 +44,14 @@ public function propertiesDataProvider() 'stateText' => array('stateText', 'test state'), 'postalCode' => array('postalCode', '12345'), 'country' => array('country', $countryMock), - 'created' => array('created', new \DateTime()), - 'updated' => array('updated', new \DateTime()), + 'created' => array('createdAt', new \DateTime()), + 'updated' => array('updatedAt', new \DateTime()), ); } public function testBeforeSave() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->beforeSave(); $this->assertNotNull($obj->getCreatedAt()); @@ -65,7 +65,7 @@ public function testBeforeSave() */ public function testToString(array $actualData, $expected) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); foreach ($actualData as $key => $value) { $setter = 'set' . ucfirst($key); @@ -129,7 +129,7 @@ protected function createMockRegion($name) public function testStateText() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $region = $this->getMockBuilder('Oro\Bundle\AddressBundle\Entity\Region') ->disableOriginalConstructor() ->getMock(); @@ -147,7 +147,7 @@ public function testIsStateValidNoCountry() $context->expects($this->never()) ->method('addViolationAt'); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->isStateValid($context); } @@ -166,7 +166,7 @@ public function testIsStateValidNoRegion() $context->expects($this->never()) ->method('addViolationAt'); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->setCountry($country); $obj->isStateValid($context); } @@ -197,14 +197,14 @@ public function testIsStateValid() array('%country%' => 'Country') ); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->setCountry($country); $obj->isStateValid($context); } public function testIsEmpty() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $this->assertTrue($obj->isEmpty()); } @@ -215,7 +215,7 @@ public function testIsEmpty() */ public function testIsNotEmpty($property, $value) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); call_user_func_array(array($obj, 'set' . ucfirst($property)), array($value)); $this->assertFalse($obj->isEmpty()); } @@ -244,33 +244,10 @@ public function emptyCheckPropertiesDataProvider() ); } - public function testIsNotEmptyFlexible() - { - $value = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexibleValue'); - $value->expects($this->once()) - ->method('getData') - ->will($this->returnValue('not empty')); - - $obj = $this->createAbstractAddress(); - $obj->addValue($value); - $this->assertFalse($obj->isEmpty()); - } - - public function testIsEmptyFlexible() - { - $value = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexibleValue'); - $value->expects($this->once()) - ->method('getData'); - - $obj = $this->createAbstractAddress(); - $obj->addValue($value); - $this->assertTrue($obj->isEmpty()); - } - /** * @return AbstractAddress|\PHPUnit_Framework_MockObject_MockObject */ - protected function createAbstractAddress() + protected function createAddress() { return $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Entity\AbstractAddress'); } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php deleted file mode 100644 index 2ef7c2a70bd..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertEquals($name, call_user_func_array(array($obj, 'get' . ucfirst($property)), array())); - - $this->assertEquals($name, $obj->getName()); - } - - /** - * Data provider - * - * @return array - */ - public function provider() - { - return array( - array('label'), - array('locale') - ); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php index e8c3bc1bd5f..463a20998ed 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php @@ -4,25 +4,18 @@ use Oro\Bundle\AddressBundle\Entity\Manager\AddressManager; use Oro\Bundle\AddressBundle\Entity\Address; -use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; -use Doctrine\Common\Persistence\ObjectManager; class AddressManagerTest extends \PHPUnit_Framework_TestCase { /** - * @var ObjectManager + * @var \PHPUnit_Framework_MockObject_MockObject */ protected $om; - /** - * @var FlexibleManager - */ - protected $fm; - /** * @var string */ - protected $class; + protected $class = 'Oro\Bundle\AddressBundle\Entity\Address'; /** * @var AddressManager @@ -35,10 +28,6 @@ class AddressManagerTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); - $this->fm = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - $this->class = 'Oro\Bundle\AddressBundle\Entity\Address'; $classMetaData = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') ->disableOriginalConstructor() @@ -55,7 +44,7 @@ public function setUp() ->with($this->equalTo($this->class)) ->will($this->returnValue($classMetaData)); - $this->addressManager = new AddressManager($this->class, $this->om, $this->fm); + $this->addressManager = new AddressManager($this->class, $this->om); } /** @@ -126,54 +115,6 @@ public function testQueryMethods() $this->assertEquals($this->addressManager->findAddressBy($addressCriteria), $address); } - /** - * Test magic call methods for existing flexible manager methods - */ - public function testCall() - { - $this->fm - ->expects($this->once()) - ->method('getFlexibleName') - ->will($this->returnValue(1)); - - $this->assertEquals($this->addressManager->getFlexibleName(), 1); - } - - /** - * Testing exception on not existing method in address manager - * - * @expectedException \RuntimeException - */ - public function testCallException() - { - $this->addressManager->NotExistingMethod(); - } - - public function testListQuery() - { - $limit = 1; - $offset = 10; - $paginator = $this->getMockBuilder('Doctrine\ORM\Tools\Pagination\Paginator') - ->disableOriginalConstructor() - ->getMock(); - - $repo = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository') - ->disableOriginalConstructor() - ->getMock(); - - $repo->expects($this->once()) - ->method('findByWithAttributesQB') - ->with(array(), null, array('id' => 'ASC'), $limit, $offset) - ->will($this->returnValue($paginator)); - - $this->fm->expects($this->once()) - ->method('getFlexibleRepository') - ->will($this->returnValue($repo)); - - - $this->assertSame($paginator, $this->addressManager->getListQuery($limit, $offset)); - } - /** * Return repository mock * diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php deleted file mode 100644 index 49c248cf30c..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php +++ /dev/null @@ -1,127 +0,0 @@ -getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = $this->createTestAddress($flexibleManager, 'oro_address_value', $buildAddressFormListener); - } - - protected function createTestAddress( - FlexibleManager $flexibleManager, - $valueFormAlias, - BuildAddressFormListener $eventListener - ) { - return $this->getMockForAbstractClass( - 'Oro\Bundle\AddressBundle\Form\Type\AbstractAddressType', - array($flexibleManager, $valueFormAlias, $eventListener) - ); - } - - public function testAddEntityFields() - { - $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $builder->expects($this->exactly(11)) - ->method('add') - ->will($this->returnSelf()); - - $builder->expects($this->at(0)) - ->method('add') - ->with('id', 'hidden'); - - $builder->expects($this->at(1)) - ->method('addEventSubscriber') - ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - - $builder->expects($this->at(2)) - ->method('add') - ->with('label', 'text'); - - $builder->expects($this->at(3)) - ->method('add') - ->with('firstName', 'text'); - - $builder->expects($this->at(4)) - ->method('add') - ->with('lastName', 'text'); - - $builder->expects($this->at(5)) - ->method('add') - ->with('street', 'text'); - - $builder->expects($this->at(6)) - ->method('add') - ->with('street2', 'text'); - - $builder->expects($this->at(7)) - ->method('add') - ->with('city', 'text'); - - $builder->expects($this->at(8)) - ->method('add') - ->with('state', 'oro_region'); - - $builder->expects($this->at(9)) - ->method('add') - ->with('state_text', 'hidden'); - - $builder->expects($this->at(10)) - ->method('add') - ->with('country', 'oro_country'); - - $builder->expects($this->at(11)) - ->method('add') - ->with('postalCode', 'text'); - - $builder->expects($this->once()) - ->method('addEventSubscriber') - ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - - $this->type->addEntityFields($builder); - } - - public function testSetDefaultOptions() - { - $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); - $resolver->expects($this->once()) - ->method('setDefaults') - ->with($this->isType('array')); - - $this->type->setDefaultOptions($resolver); - } - - public function testGetName() - { - $this->setExpectedException( - 'BadMethodCallException', - sprintf('Method %s::getName() must be overridden.', get_class($this->type)) - ); - $this->type->getName(); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php index d99b7bba924..05535851ac3 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php @@ -16,56 +16,42 @@ class AbstractTypedAddressTypeTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - $buildAddressFormListener - = $this->getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = $this->getMockForAbstractClass( - 'Oro\Bundle\AddressBundle\Form\Type\AbstractTypedAddressType', - array( - $flexibleManager, - 'oro_address_value', - $buildAddressFormListener - ) - ); + $this->type = $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Form\Type\AbstractTypedAddressType'); } - public function testAddEntityFields() + public function testBuildForm() { $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') ->disableOriginalConstructor() ->getMock(); - $builder->expects($this->any()) - ->method('add') - ->will($this->returnSelf()); + $builder->expects($this->at(0)) ->method('add') ->with( 'types', 'translatable_entity', - $this->callback( - function ($options) { - \PHPUnit_Framework_TestCase::assertArrayHasKey('class', $options); - \PHPUnit_Framework_TestCase::assertArrayHasKey('property', $options); - \PHPUnit_Framework_TestCase::assertEquals('OroAddressBundle:AddressType', $options['class']); - \PHPUnit_Framework_TestCase::assertEquals('label', $options['property']); - return true; - } + array( + 'class' => 'OroAddressBundle:AddressType', + 'property' => 'label', + 'required' => false, + 'multiple' => true, + 'expanded' => true, ) - ); + ) + ->will($this->returnSelf()); $builder->expects($this->at(1)) ->method('add') ->with( 'primary', 'checkbox', - $this->isType('array') - ); + array( + 'label' => 'Primary', + 'required' => false + ) + ) + ->will($this->returnSelf()); - $this->type->addEntityFields($builder); + $this->type->buildForm($builder, array()); } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php index 7d269a05399..ba8bccaec07 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php @@ -20,45 +20,32 @@ class AddressApiTypeTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - $buildAddressFormListener = $this->getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = new AddressApiType( - $flexibleManager, - 'oro_address_value', - $buildAddressFormListener - ); + $this->type = new AddressApiType(); } - public function testAddEntityFields() + public function testBuildForm() { - /** @var \Symfony\Component\Form\FormBuilderInterface $builder */ $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') ->disableOriginalConstructor() ->getMock(); - $builder->expects($this->exactly(11)) - ->method('add') - ->will($this->returnSelf()); - - $builder->expects($this->exactly(2)) + $builder->expects($this->once()) ->method('addEventSubscriber') ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - $this->type->addEntityFields($builder); + $this->type->buildForm($builder, array()); } public function testSetDefaultOptions() { - /** @var OptionsResolverInterface $resolver */ $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); $resolver->expects($this->once()) ->method('setDefaults') - ->with($this->isType('array')); + ->with( + array( + 'csrf_protection' => false, + ) + ); $this->type->setDefaultOptions($resolver); } @@ -66,4 +53,9 @@ public function testGetName() { $this->assertEquals('address', $this->type->getName()); } + + public function testGetParent() + { + $this->assertEquals('oro_address', $this->type->getParent()); + } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php index 9c527548c29..b4b987c9633 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php @@ -1,8 +1,6 @@ getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); $resolver->expects($this->once()) ->method('setDefaults') diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php index 2c02a7cceb5..f0b8d8d90e1 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php @@ -2,22 +2,96 @@ namespace Oro\Bundle\AddressBundle\Tests\Unit\Form\Type; use Oro\Bundle\AddressBundle\Form\Type\AddressType; -use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; -use Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener; -class AddressTypeTest extends AbstractAddressTypeTest +class AddressTypeTest extends \PHPUnit_Framework_TestCase { /** - * @var AbstractAddressTypeTest + * @var AddressType */ protected $type; - protected function createTestAddress( - FlexibleManager $flexibleManager, - $valueFormAlias, - BuildAddressFormListener $eventListener - ) { - return new AddressType($flexibleManager, $valueFormAlias, $eventListener); + /** + * Setup test env + */ + public function setUp() + { + $buildAddressFormListener = $this->getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') + ->disableOriginalConstructor() + ->getMock(); + + $this->type = new AddressType($buildAddressFormListener); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(11)) + ->method('add') + ->will($this->returnSelf()); + + $builder->expects($this->at(0)) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')) + ->will($this->returnSelf()); + + $builder->expects($this->at(1)) + ->method('add') + ->with('id', 'hidden'); + + $builder->expects($this->at(2)) + ->method('add') + ->with('label', 'text'); + + $builder->expects($this->at(3)) + ->method('add') + ->with('firstName', 'text'); + + $builder->expects($this->at(4)) + ->method('add') + ->with('lastName', 'text'); + + $builder->expects($this->at(5)) + ->method('add') + ->with('street', 'text'); + + $builder->expects($this->at(6)) + ->method('add') + ->with('street2', 'text'); + + $builder->expects($this->at(7)) + ->method('add') + ->with('city', 'text'); + + $builder->expects($this->at(8)) + ->method('add') + ->with('state', 'oro_region'); + + $builder->expects($this->at(9)) + ->method('add') + ->with('state_text', 'hidden'); + + $builder->expects($this->at(10)) + ->method('add') + ->with('country', 'oro_country'); + + $builder->expects($this->at(11)) + ->method('add') + ->with('postalCode', 'text'); + + $this->type->buildForm($builder, array()); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); } public function testGetName() diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php deleted file mode 100644 index b30a6db8d04..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php +++ /dev/null @@ -1,112 +0,0 @@ -extension = new HasAddressExtension(); - $this->entityMock = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexible'); - $this->valuesMock = $this->getMock('Doctrine\Common\Collections\ArrayCollection'); - $this->valueMock = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Model\FlexibleValueInterface'); - } - - public function testGetName() - { - $this->assertEquals('oro_address_hasAddress', $this->extension->getName()); - } - - public function testGetFilters() - { - $filters = $this->extension->getFilters(); - - $this->assertArrayHasKey('hasAddress', $filters); - $this->assertInstanceOf('\Twig_Filter_Method', $filters['hasAddress']); - } - - public function testHasAddressPositiveScenario() - { - $this->valueMock->expects($this->once()) - ->method('getData') - ->will($this->returnValue(true)); - - $this->entityMock->expects($this->once()) - ->method('getValue') - ->with($this->equalTo('address')) - ->will($this->returnValue($this->valueMock)); - - $this->assertTrue($this->extension->hasAddress($this->entityMock, 'address')); - } - - public function testHasAddressNegativeScenario() - { - $this->valueMock->expects($this->once()) - ->method('getData') - ->will($this->returnValue(null)); - - $this->entityMock->expects($this->once()) - ->method('getValue') - ->with($this->equalTo('address')) - ->will($this->returnValue($this->valueMock)); - - $this->assertFalse($this->extension->hasAddress($this->entityMock, 'address')); - } - - public function testHasAddressPositiveScenarioWithoutCode() - { - $this->entityMock->expects($this->once()) - ->method('getValues') - ->will($this->returnValue($this->valuesMock)); - - $this->valuesMock->expects($this->once()) - ->method('filter') - ->will($this->returnValue($this->valuesMock)); - $this->valuesMock->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(false)); - - $this->assertTrue($this->extension->hasAddress($this->entityMock)); - } - - public function testHasAddressNegativeScenarioWithoutCode() - { - $this->entityMock->expects($this->once()) - ->method('getValues') - ->will($this->returnValue($this->valuesMock)); - - $this->valuesMock->expects($this->once()) - ->method('filter') - ->will($this->returnValue($this->valuesMock)); - $this->valuesMock->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(true)); - - $this->assertFalse($this->extension->hasAddress($this->entityMock)); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php b/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php deleted file mode 100644 index bdb44400038..00000000000 --- a/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php +++ /dev/null @@ -1,55 +0,0 @@ - new \Twig_Filter_Method($this, 'hasAddress') - ); - } - - /** - * Check whenever flexible entity contains not empty address attribute - * - * @param \Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexible $entity - * @param null $addressCode - * @return bool - */ - public function hasAddress(AbstractEntityFlexible $entity, $addressCode = null) - { - if ($addressCode !== null) { - $address = $entity->getValue($addressCode); - - return $address->getData() != null; - } - - /** @var \Doctrine\Common\Collections\ArrayCollection $values **/ - $values = $entity->getValues(); - $values = $values->filter( - function ($value) { - if (strpos($value->getAttribute()->getCode(), 'address') !== false) { - return $value->getData() != null; - } - - return false; - } - ); - return !$values->isEmpty(); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'oro_address_hasAddress'; - } -} diff --git a/src/Oro/Bundle/AddressBundle/composer.json b/src/Oro/Bundle/AddressBundle/composer.json index 02065ac4879..77fdf8a6dfd 100644 --- a/src/Oro/Bundle/AddressBundle/composer.json +++ b/src/Oro/Bundle/AddressBundle/composer.json @@ -13,7 +13,6 @@ "friendsofsymfony/rest-bundle": "0.11.*", "friendsofsymfony/jsrouting-bundle": "1.1.*@dev", "nelmio/api-doc-bundle": "dev-master", - "oro/flexible-entity-bundle": "dev-master", "oro/form-bundle": "dev-master", "oro/translation-bundle": "dev-master", "besimple/soap-bundle": "dev-master" From bfa80274d01ce17d151b69b3199923e731a13702 Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Fri, 2 Aug 2013 18:11:16 +0300 Subject: [PATCH 065/541] ConfigManager merge --- .../EntityConfigBundle/ConfigManager.php | 80 ++++++++++++++----- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php index 9517b3f7873..e1b66690da0 100644 --- a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php @@ -30,7 +30,6 @@ use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; use Oro\Bundle\EntityConfigBundle\Event\PersistConfigEvent; -use Oro\Bundle\EntityConfigBundle\Event\FlushConfigEvent; use Oro\Bundle\EntityConfigBundle\Event\NewConfigModelEvent; use Oro\Bundle\EntityConfigBundle\Event\Events; @@ -94,7 +93,7 @@ class ConfigManager /** * @var AbstractConfigModel[] */ - protected $entities = array(); + protected $models = array(); /** * @var ConfigInterface[] @@ -283,14 +282,17 @@ public function createConfigEntityModel($className) /** @var ConfigClassMetadata $metadata */ $metadata = $this->metadataFactory->getMetadataForClass($className); + $this->models[$className] = new EntityConfigModel($className); + foreach ($this->getProviders() as $provider) { $defaultValues = array(); if (isset($metadata->defaultValues[$provider->getScope()])) { $defaultValues = $metadata->defaultValues[$provider->getScope()]; } - $entityId = new EntityConfigId($className, $provider->getScope()); - $provider->createConfig($entityId, $defaultValues); + $entityId = new EntityConfigId($className, $provider->getScope()); + $config = $provider->createConfig($entityId, $defaultValues); + $this->originalConfigs[$config->getConfigId()->getId()] = clone $config; $this->eventDispatcher->dispatch(Events::NEW_CONFIG_MODEL, new NewConfigModelEvent($entityId, $this)); } @@ -301,6 +303,7 @@ public function createConfigEntityModel($className) * @param $className * @param $fieldName * @param $fieldType + * @throws Exception\LogicException */ public function createConfigFieldModel($className, $fieldName, $fieldType) { @@ -308,24 +311,33 @@ public function createConfigFieldModel($className, $fieldName, $fieldType) /** @var ConfigClassMetadata $metadata */ //$metadata = $this->metadataFactory->getMetadataForClass($className); //TODO::implement default value for config + /** @var EntityConfigModel $entityModel */ + $entityModel = isset($this->models[$className]) ? $this->models[$className] : $this->getConfigModel($className); + if (!$entityModel) { + throw new LogicException(sprintf('Entity "%" is not found', $className)); + } + + $this->models[$className . $fieldName] = $fieldModel = new FieldConfigModel($fieldName, $fieldType); + + $entityModel->addField($fieldModel); + foreach ($this->getProviders() as $provider) { $entityId = new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType); $provider->createConfig($entityId, array()); - $provider->createConfig(new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType), array()); + $config = $provider->createConfig(new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType), array()); + $this->originalConfigs[$config->getConfigId()->getId()] = clone $config; $this->eventDispatcher->dispatch(Events::NEW_CONFIG_MODEL, new NewConfigModelEvent($entityId, $this)); } } } - /** * @param ConfigIdInterface $configId */ public function clearCache(ConfigIdInterface $configId) { - if ($this->configCache) { $this->configCache->removeConfigFromCache($configId); } @@ -336,29 +348,35 @@ public function clearCache(ConfigIdInterface $configId) */ public function persist(ConfigInterface $config) { - $this->persistConfigs->push($config); - - if ($config instanceof EntityConfigInterface) { - foreach ($config->getFields() as $fieldConfig) { - $this->persistConfigs->push($fieldConfig); - } + if (isset($this->originalConfigs[$config->getConfigId()->getId()])) { + $this->insertConfigs[$config->getConfigId()->getId()] = $config; + } else { + $this->updateConfigs[$config->getConfigId()->getId()] = $config; } } /** * @param ConfigInterface $config */ - public function remove(ConfigInterface $config) + public function merge(ConfigInterface $config) { - $this->removeConfigs[spl_object_hash($config)] = $config; + $config = $this->doMerge($config); - if ($config instanceof EntityConfigInterface) { - foreach ($config->getFields() as $fieldConfig) { - $this->removeConfigs[spl_object_hash($fieldConfig)] = $fieldConfig; - } + if (isset($this->originalConfigs[$config->getConfigId()->getId()])) { + $this->insertConfigs[$config->getConfigId()->getId()] = $config; + } else { + $this->updateConfigs[$config->getConfigId()->getId()] = $config; } } + /** + * @param ConfigInterface $config + */ + public function remove(ConfigInterface $config) + { + $this->removeConfigs[$config->getConfigId()->getId()] = $config; + } + /** * TODO:: remove configs */ @@ -547,8 +565,8 @@ protected function getConfigModel($className, $fieldName = null) { $id = $className . $fieldName; - if (isset($this->entities[$id])) { - return $this->entities[$id]; + if (isset($this->models[$id])) { + return $this->models[$id]; } $entityConfigRepo = $this->em()->getRepository(EntityConfigModel::ENTITY_NAME); @@ -567,4 +585,24 @@ protected function getConfigModel($className, $fieldName = null) return $result; } + + /** + * @param ConfigInterface $config + * @return ConfigInterface + */ + protected function doMerge(ConfigInterface $config) + { + switch (true) { + case isset($this->insertConfigs[$config->getConfigId()->getId()]): + $persistConfig = $this->insertConfigs[$config->getConfigId()->getId()]; + break; + case isset($this->updateConfigs[$config->getConfigId()->getId()]): + $persistConfig = $this->insertConfigs[$config->getConfigId()->getId()]; + break; + default: + return $config; + } + + return array_merge($persistConfig->getValues(), $config->getValues()); + } } From 06fdfdb02ea359adff7c28c637911f26daf51c13 Mon Sep 17 00:00:00 2001 From: Dmitry Khrysev Date: Fri, 2 Aug 2013 18:31:34 +0300 Subject: [PATCH 066/541] CRM-328: Exrtract abstract JS widget functionality - added block widget - added ability to disable hash navigation on form --- .../Resources/public/js/hash.navigation.js | 3 ++ .../public/js/backbone/widget/abstract.js | 32 ++++++++++++------- .../public/js/backbone/widget/block.js | 32 +++++++++++-------- .../Resources/public/js/views/dialog.js | 7 ++-- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js index 8bdf30970a1..bd67e07aaf5 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/hash.navigation.js @@ -1018,6 +1018,9 @@ Oro.Navigation = Backbone.Router.extend({ processForms: function(selector) { $(selector).on('submit', _.bind(function (e) { var target = e.currentTarget; + if (target.data('nohash')) { + return; + } e.preventDefault(); var url = $(target).attr('action'); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js index b56bb5eab9d..bb18d83c969 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js @@ -14,10 +14,6 @@ Oro.widget.Abstract = Backbone.View.extend({ console.warn('Implement setTitle'); }, - show: function() { - console.warn('Implement show'); - }, - renderActions: function() { console.warn('Implement renderActions'); }, @@ -32,7 +28,6 @@ Oro.widget.Abstract = Backbone.View.extend({ this.actions = []; this.firstRun = true; - this.widgetContent = this.$el; }, getWid: function() { @@ -66,7 +61,7 @@ Oro.widget.Abstract = Backbone.View.extend({ */ adoptFormActions: function() { this._initEmbeddedForm(); - if (this.form !== undefined) { + if (this.hasAdoptedActions && this.form !== undefined) { var actions = this._getActionsElement(); var self = this; actions.find('[type=submit]').each(function(idx, btn) { @@ -75,7 +70,8 @@ Oro.widget.Abstract = Backbone.View.extend({ return false; }); }); - this.form.submit(function() { + this.form.submit(function(e) { + e.stopImmediatePropagation(); self.trigger('adoptedFormSubmit', self.form, self); return false; }); @@ -127,16 +123,22 @@ Oro.widget.Abstract = Backbone.View.extend({ return false; }, - addAction: function(actionElement) { - this.actions.push(actionElement); - this.renderActions(); + addAction: function(key, actionElement) { + if (!this.hasAction(key)) { + this.actions[key] = actionElement; + this._getActionsElement().append(actionElement); + } + }, + + hasAction: function(key) { + return this.actions.hasOwnProperty(key); }, getPreparedActions: function() { this.adoptFormActions(); var container = this._getActionsElement(); - for (var i = 0; i < this.actions.length; i++) { - container.append(this.actions[i]); + for (var actionKey in this.actions) if (this.actions.hasOwnProperty(actionKey)) { + container.append(this.actions[actionKey]); } return container; }, @@ -193,4 +195,10 @@ Oro.widget.Abstract = Backbone.View.extend({ } }, this)); }, + + show: function() { + this.renderActions(); + this.$el.trigger('widgetize', this); + this.trigger('widgetRender', this.widgetContent, this); + } }); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js index 4681d5bb646..0357ed75d5f 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js @@ -3,34 +3,30 @@ Oro.widget = Oro.widget || {}; Oro.widget.Block = Oro.widget.Abstract.extend({ options: _.extend( - Oro.widget.Abstract.prototype.options, { type: 'block', - titleEl: '.widget-title', - actionsEl: '.widget-actions', - contentEl: '.box-content', + titleContainer: '.widget-title', + actionsContainer: '.widget-actions-container', + contentContainer: '.box-content', template: _.template('
      ' + '
      ' + - '
      ' + + '
      ' + '<%- title %>' + '
      ' + '
      ' + '
      ') - } + }, + Oro.widget.Abstract.prototype.options ), initialize: function(options) { options = options || {} this.initializeWidget(options); - var anchorDiv = $('
      '); - anchorDiv.after(this.$el); this.widget = Backbone.$(this.options.template({ 'title': this.options.title })); - this.widgetContent = this.widget.find(this.options.contentEl); - this.widgetContent.append(this.$el); - anchorDiv.replaceWith(this.widget); + this.widgetContent = this.widget.find(this.options.contentContainer); }, setTitle: function(title) { @@ -39,25 +35,33 @@ Oro.widget.Block = Oro.widget.Abstract.extend({ }, renderActions: function() { + this._getActionsContainer().empty(); this._getActionsContainer().append(this.getPreparedActions()); }, _getActionsContainer: function() { if (this.actionsContainer === undefined) { - this.actionsContainer = this.widget.find(this.options.actionsEl); + this.actionsContainer = this.widget.find(this.options.actionsContainer); } return this.actionsContainer; }, _getTitleContainer: function() { if (this.titleContainer === undefined) { - this.titleContainer = this.widget.find(this.options.titleEl); + this.titleContainer = this.widget.find(this.options.titleContainer); } return this.titleContainer; }, show: function() { - this.renderActions(); + if (!this.$el.data('wid')) { + this.$el.attr('data-wid', this.getWid()); + var anchorDiv = Backbone.$('
      '); + anchorDiv.insertAfter(this.$el); + this.widgetContent.append(this.$el); + anchorDiv.replaceWith($(this.widget)); + } + Oro.widget.Abstract.prototype.show.apply(this); } }); diff --git a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js index cbefa380e75..9ade3ce864a 100644 --- a/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js +++ b/src/Oro/Bundle/WindowsBundle/Resources/public/js/views/dialog.js @@ -3,11 +3,11 @@ Oro.widget = Oro.widget || {}; Oro.widget.DialogView = Oro.widget.Abstract.extend({ options: _.extend( - Oro.widget.Abstract.prototype.options, { type: 'dialog', dialogOptions: null - } + }, + Oro.widget.Abstract.prototype.options ), // Windows manager global variables @@ -97,6 +97,7 @@ Oro.widget.DialogView = Oro.widget.Abstract.extend({ }); this.widgetContent.remove(); this._getActionsElement().remove(); + this.widget.remove(); }, handleStateChange: function(e, data) { @@ -155,7 +156,7 @@ Oro.widget.DialogView = Oro.widget.Abstract.extend({ } else { this.widget.html(this.widgetContent); } - this.renderActions(); + Oro.widget.Abstract.prototype.show.apply(this); }, /** From e75196a7fab1cb58be201bac87308030b9544fbb Mon Sep 17 00:00:00 2001 From: Falko Konstantin Date: Fri, 2 Aug 2013 18:48:45 +0300 Subject: [PATCH 067/541] ConfigManager get updated configs --- .../EntityConfigBundle/Audit/AuditManager.php | 2 +- .../EntityConfigBundle/ConfigManager.php | 94 +++++++++---------- .../Tests/Unit/ConfigManagerTest.php | 10 +- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php index cc630384793..601bbdfb9de 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php @@ -46,7 +46,7 @@ public function log() $log = new ConfigLog(); $log->setUser($this->getUser()); - foreach (array_merge($this->configManager->getUpdatedEntityConfig(), $this->configManager->getUpdatedFieldConfig()) as $config) { + foreach (array_merge($this->configManager->getUpdatedConfig(), $this->configManager->getInsertConfig()) as $config) { $this->logConfig($config, $log); } diff --git a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php index e1b66690da0..7a3e13f6c0b 100644 --- a/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/ConfigManager.php @@ -65,11 +65,6 @@ class ConfigManager */ protected $providers = array(); - /** - * @var ConfigInterface[] - */ - protected $persistConfigs = array(); - /** * @var ConfigInterface[] */ @@ -105,11 +100,6 @@ class ConfigManager */ protected $configChangeSets = array(); - /** - * @var array - */ - protected $updatedConfigs = array(); - /** * @param MetadataFactory $metadataFactory * @param EventDispatcher $eventDispatcher @@ -437,9 +427,10 @@ public function flush() $this->eventDispatcher->dispatch(Events::ON_FLUSH, new FlushConfigEvent($this)); $this->removeConfigs = array(); + $this->insertConfigs = array(); + $this->updateConfigs = array(); $this->originalConfigs = array(); $this->configChangeSets = array(); - $this->updatedConfigs = array(); $this->eventDispatcher->dispatch(Events::POST_FLUSH, new FlushConfigEvent($this)); @@ -452,8 +443,8 @@ public function flush() public function calculateConfigChangeSet(ConfigInterface $config) { $originConfigValue = array(); - if (isset($this->originalConfigs[spl_object_hash($config)])) { - $originConfig = $this->originalConfigs[spl_object_hash($config)]; + if (isset($this->originalConfigs[$config->getConfigId()->getId()])) { + $originConfig = $this->originalConfigs[$config->getConfigId()->getId()]; $originConfigValue = $originConfig->getValues(); } @@ -478,60 +469,67 @@ public function calculateConfigChangeSet(ConfigInterface $config) } - if (!isset($this->configChangeSets[spl_object_hash($config)])) { - $this->configChangeSets[spl_object_hash($config)] = array(); + if (!isset($this->configChangeSets[$config->getConfigId()->getId()])) { + $this->configChangeSets[$config->getConfigId()->getId()] = array(); } if (count($diff)) { - $this->configChangeSets[spl_object_hash($config)] = array_merge($this->configChangeSets[spl_object_hash($config)], $diff); - - if (!isset($this->updatedConfigs[spl_object_hash($config)])) { - $this->updatedConfigs[spl_object_hash($config)] = $config; - } + $this->configChangeSets[$config->getConfigId()->getId()] = array_merge($this->configChangeSets[$config->getConfigId()->getId()], $diff); } } /** - * @param null $scope - * @return ConfigInterface[]|EntityConfigInterface[] + * @param callback $filter + * @throws Exception\RuntimeException + * @return ConfigInterface[]| */ - public function getUpdatedEntityConfig($scope = null) + public function getUpdatedConfig($filter = null) { - return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($scope) { - if (!$config instanceof EntityConfigInterface) { - return false; - } + if ($filter && !is_callable($filter)) { + throw new RuntimeException(sprintf('Expect callable $filter given "%s"', gettype($filter))); + } - if ($scope && $config->getScope() != $scope) { - return false; - } + if ($filter) { + return array_filter($this->updateConfigs, $filter); + } - return true; - }); + return $this->updateConfigs; } /** - * @param null $className - * @param null $scope - * @return ConfigInterface[]|FieldConfigInterface[] + * @param callback $filter + * @throws Exception\RuntimeException + * @return ConfigInterface[]| */ - public function getUpdatedFieldConfig($scope = null, $className = null) + public function getInsertConfig($filter = null) { - return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($className, $scope) { - if (!$config instanceof FieldConfigInterface) { - return false; - } + if ($filter && !is_callable($filter)) { + throw new RuntimeException(sprintf('Expect callable $filter given "%s"', gettype($filter))); + } - if ($className && $config->getClassName() != $className) { - return false; - } + if ($filter) { + return array_filter($this->insertConfigs, $filter); + } - if ($scope && $config->getScope() != $scope) { - return false; - } + return $this->insertConfigs; + } - return true; - }); + /** + * @param callback $filter + * @throws Exception\RuntimeException + * @return ConfigInterface[]| + */ + public function getRemoveConfig($filter = null) + { + if ($filter && !is_callable($filter)) { + throw new RuntimeException(sprintf('Expect callable $filter given "%s"', gettype($filter))); + } + + if ($filter) { + return array_filter($this->removeConfigs, $filter); + } + + return $this->removeConfigs; } /** diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php index 5e8515a06e4..e542125dac8 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php @@ -243,12 +243,12 @@ public function testChangeSet() $this->assertEquals($result, $this->configManager->getConfigChangeSet($config)); - $this->assertEquals(array(spl_object_hash($config) => $config), $this->configManager->getUpdatedEntityConfig()); - $this->assertEquals(array(), $this->configManager->getUpdatedEntityConfig('test1')); + $this->assertEquals(array(spl_object_hash($config) => $config), $this->configManager->getUpdatedConfig()); + $this->assertEquals(array(), $this->configManager->getUpdatedConfig('test1')); - $this->assertEquals(array(spl_object_hash($configField) => $configField), $this->configManager->getUpdatedFieldConfig()); - $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig('test1')); - $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig(null, 'WrongClass')); + $this->assertEquals(array(spl_object_hash($configField) => $configField), $this->configManager->getInsertConfig()); + $this->assertEquals(array(), $this->configManager->getInsertConfig('test1')); + $this->assertEquals(array(), $this->configManager->getInsertConfig(null, 'WrongClass')); } protected function initConfigManager() From 9d31dc72ce79df03685f4080be9d57d72b7e9da7 Mon Sep 17 00:00:00 2001 From: Dmitry Khrysev Date: Fri, 2 Aug 2013 19:04:43 +0300 Subject: [PATCH 068/541] CRM-328: Exrtract abstract JS widget functionality - windows states create windowns with widget manager --- .../UIBundle/Resources/public/js/backbone/widget/abstract.js | 1 + .../UIBundle/Resources/public/js/backbone/widget/block.js | 1 - src/Oro/Bundle/WindowsBundle/Resources/views/states.html.twig | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js index bb18d83c969..ebf1a8bbce8 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js @@ -197,6 +197,7 @@ Oro.widget.Abstract = Backbone.View.extend({ }, show: function() { + this.$el.attr('data-wid', this.getWid()); this.renderActions(); this.$el.trigger('widgetize', this); this.trigger('widgetRender', this.widgetContent, this); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js index 0357ed75d5f..44a1dd2f9db 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js @@ -55,7 +55,6 @@ Oro.widget.Block = Oro.widget.Abstract.extend({ show: function() { if (!this.$el.data('wid')) { - this.$el.attr('data-wid', this.getWid()); var anchorDiv = Backbone.$('
      '); anchorDiv.insertAfter(this.$el); this.widgetContent.append(this.$el); diff --git a/src/Oro/Bundle/WindowsBundle/Resources/views/states.html.twig b/src/Oro/Bundle/WindowsBundle/Resources/views/states.html.twig index f971d087f84..914b4f51287 100644 --- a/src/Oro/Bundle/WindowsBundle/Resources/views/states.html.twig +++ b/src/Oro/Bundle/WindowsBundle/Resources/views/states.html.twig @@ -13,7 +13,7 @@ diff --git a/src/Oro/Bundle/UIBundle/Twig/WidgetExtension.php b/src/Oro/Bundle/UIBundle/Twig/WidgetExtension.php new file mode 100644 index 00000000000..559d789d182 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Twig/WidgetExtension.php @@ -0,0 +1,119 @@ + new \Twig_Function_Method( + $this, + 'render', + array( + 'is_safe' => array('html'), + 'needs_environment' => true + ) + ) + ); + } + + /** + * Renders a widget. + * + * @param \Twig_Environment $environment + * @param array $options + * + * @throws \InvalidArgumentException + * @return string + */ + public function render(Twig_Environment $environment, array $options = array()) + { + if ($this->rendered) { + return ''; + } + $this->rendered = true; + + if (!array_key_exists('url', $options)) { + throw new \InvalidArgumentException('Option url is required'); + } + if (!array_key_exists('widgetType', $options)) { + throw new \InvalidArgumentException('Option widgetType is required'); + } else { + $widgetType = $options['widgetType']; + unset($options['widgetType']); + } + if (!array_key_exists('elementFirst', $options)) { + $options['elementFirst'] = true; + } + $options['wid'] = $this->getUniqueIdentifier(); + $elementId = 'widget-container-' . $options['wid']; + $options['el'] = '#' . $elementId . ' .widget-content'; + $options['url'] = $this->getUrlWithContainer($options['url'], $widgetType, $options['wid']); + + return $environment->render( + "OroUIBundle::widget_loader.html.twig", + array( + "widgetType" => $widgetType, + "elementId" => $elementId, + "options" => $options + ) + ); + } + + /** + * @param string $url + * @param string $widgetType + * @param string $wid + * @return string + */ + protected function getUrlWithContainer($url, $widgetType, $wid) + { + if (strpos($url, '_widgetContainer=') === false) { + $parts = parse_url($url); + $widgetPart = '_widgetContainer=' . $widgetType . '&_wid=' . $wid; + if (array_key_exists('query', $parts)) { + $separator = $parts['query'] ? '&' : ''; + $newQuery = $parts['query'] . $separator . $widgetPart; + $url = str_replace($parts['query'], $newQuery, $url); + } else { + $url .= '?' . $widgetPart; + } + } + return $url; + } + + /** + * @return string + */ + protected function getUniqueIdentifier() + { + return str_replace('.', '-', uniqid('', true)); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return self::EXTENSION_NAME; + } +} From fbb1a4ee79374d25153f7e36cf7ad6edfc9827a2 Mon Sep 17 00:00:00 2001 From: Hryhorii Hrebiniuk Date: Mon, 5 Aug 2013 14:48:24 +0300 Subject: [PATCH 094/541] Implemented BAP-1291: Filter by selected/not selected records --- .../GridBundle/Resources/config/assets.yml | 1 + .../js/app/datagrid/cell/selectallheader.js | 9 +- .../js/app/datagrid/filter/selectrowfilter.js | 104 ++++++++++++++++++ .../views/Include/javascript.html.twig | 14 +++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/filter/selectrowfilter.js diff --git a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml index 90058198ad6..823f1162820 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml @@ -40,6 +40,7 @@ js: - '@OroGridBundle/Resources/public/js/app/datagrid/toolbar.js' - '@OroGridBundle/Resources/public/js/app/datagrid/filter/list.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/filter/selectrowfilter.js' - '@OroGridBundle/Resources/public/js/app/loadingmask.js' - '@OroGridBundle/Resources/public/js/app/datagrid/grid.js' diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js index 33b2e696506..d9cd25d35f5 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js @@ -1,7 +1,7 @@ /* jshint browser:true */ (function (factory) { "use strict"; - /* global define, Oro, jQuery, _, Backbone, Backgrid */ + /* global define, Oro, jQuery, _, Backgrid */ if (typeof define === 'function' && define.amd) { define(['Oro', 'jQuery', '_', 'Backgrid', 'OroDatagridCellSelectRowCell'], factory); } else { @@ -157,7 +157,10 @@ render: function () { /*jshint multistr:true */ /*jslint es5: true */ - /* render method will detend on options or will be empty */ + /* temp solution: start */ + // It's not clear for now, how mass selection will be designed, + // thus implementation is done just to check functionality. + // For future render method will depend on options or will be empty this.$el.empty().append('
      \ \ ' + '' + '
      ' diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js index 57e1000c9a9..9eee74a97c9 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js @@ -20,6 +20,9 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ /** @property */ enabled: true, + /** @property */ + enabledNow: true, + /** @property */ template: _.template( '' + @@ -77,6 +80,9 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ this.listenTo(this.collection, "add", this.render); this.listenTo(this.collection, "remove", this.render); this.listenTo(this.collection, "reset", this.render); + + this.enabled = options.enable != false; + Backbone.View.prototype.initialize.call(this, options); }, @@ -86,7 +92,7 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ * @return {*} */ disable: function() { - this.enabled = false; + this.enabledNow = false; this.render(); return this; }, @@ -96,8 +102,12 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ * * @return {*} */ - enable: function() { - this.enabled = true; + enable: function(force) { + if (force == undefined && !this.enabled) { + return false; + } + + this.enabledNow = true; this.render(); return this; }, diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js index 1c6fd01cf65..602e535a791 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js @@ -60,14 +60,12 @@ Oro.Datagrid.Toolbar = Backbone.View.extend({ this.collection = options.collection; - this.pagination = new this.pagination({ - collection: this.collection - }); + this.pagination = new this.pagination(_.extend({}, options.pagination, { collection: this.collection })); options.pageSize = options.pageSize || {}; - this.pageSize = new this.pageSize( _.extend({}, options.pageSize, { collection: this.collection }) ); + this.pageSize = new this.pageSize(_.extend({}, options.pageSize, { collection: this.collection })); - this.actionsPanel = new this.actionsPanel(); + this.actionsPanel = new this.actionsPanel(_.extend({}, options.actionsPanel)); if (options.actions) { this.actionsPanel.setActions(options.actions); } From d751985988b662a6f80c549311ed5bd2d74aebf4 Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Mon, 5 Aug 2013 18:05:47 +0200 Subject: [PATCH 104/541] BAP-1233: Wrong behavior on adding new tags during creating new entity - Added unit tests --- .../Bundle/TagBundle/Entity/TagManager.php | 150 +++++++------- .../Resources/views/Form/fields.html.twig | 3 +- .../Tests/Unit/Entity/TagManagerTest.php | 191 +++++++++++++++++- .../Tests/Unit/Fixtures/Taggable.php | 71 +++++++ 4 files changed, 328 insertions(+), 87 deletions(-) create mode 100644 src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Taggable.php diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index cefc4e438e3..7ab29475629 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -168,6 +168,7 @@ function (Tagging $tagging) use ($entity) { && $tagging->getRecordId() == $entity->getTaggableId(); } ); + /** @var Tagging $tagging */ foreach ($taggingCollection as $tagging) { if ($this->getUser()->getId() == $tagging->getCreatedBy()->getId()) { @@ -192,96 +193,87 @@ public function saveTagging(Taggable $resource) { $oldTags = $this->getTagging($resource, $this->getUser()->getId()); $newTags = $resource->getTags(); - - if (!isset($newTags['all'], $newTags['owner'])) { - return; - } - - // allow adding only 'my' tags - $newOwnerTags = new ArrayCollection($newTags['owner']); - - // find new - $tagsToAdd = new ArrayCollection(); - foreach ($newOwnerTags as $newOwnerTag) { - $callback = function ($index, $oldTag) use ($newOwnerTag) { - return $oldTag->getName() == $newOwnerTag->getName(); - }; - - if (!$oldTags->exists($callback)) { - $tagsToAdd->add($newOwnerTag); - } - } - - // find removed - $tagsToRemove = array(); - foreach ($oldTags as $oldTag) { - $callback = function ($index, $newTag) use ($oldTag) { - return $newTag->getName() == $oldTag->getName(); - }; - - if (!$newOwnerTags->exists($callback)) { - $tagsToRemove[] = $oldTag->getId(); - } - } - - if (sizeof($tagsToRemove)) { - $this->deleteTaggingByParams( - $tagsToRemove, - get_class($resource), - $resource->getTaggableId(), - $this->getUser()->getId() + if (isset($newTags['all'], $newTags['owner'])) { + $newOwnerTags = new ArrayCollection($newTags['owner']); + $newAllTags = new ArrayCollection($newTags['all']); + + $manager = $this; + $tagsToAdd = $newOwnerTags->filter( + function ($tag) use ($oldTags, $manager) { + return !$oldTags->exists($manager->compareCallback($tag)); + } ); - } - - // process if current user allowed to remove other's tag links - if ($this->aclManager->isResourceGranted(self::ACL_RESOURCE_REMOVE_ID_KEY)) { - $newAllTags = new ArrayCollection($newTags['all']); - // get 'not mine' taggings - $oldTags = $this->getTagging($resource, $this->getUser()->getId(), true); - $tagsToRemove = array(); - - foreach ($oldTags as $oldTag) { - $callback = function ($index, $newTag) use ($oldTag) { - return $newTag->getName() == $oldTag->getName(); - }; - - if (!$newAllTags->exists($callback)) { - $tagsToRemove[] = $oldTag->getId(); + $tagsToDelete = $oldTags->filter( + function ($tag) use ($newOwnerTags, $manager) { + return !$newOwnerTags->exists($manager->compareCallback($tag)); } - } + ); - if (count($tagsToRemove) > 0) { + if (!$tagsToDelete->isEmpty() + && $this->aclManager->isResourceGranted(self::ACL_RESOURCE_ASSIGN_ID_KEY) + ) { $this->deleteTaggingByParams( - $tagsToRemove, + $tagsToDelete, get_class($resource), - $resource->getTaggableId() + $resource->getTaggableId(), + $this->getUser()->getId() ); } - } - foreach ($tagsToAdd as $tag) { - if (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_ASSIGN_ID_KEY) - || (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_CREATE_ID_KEY) && !$tag->getId()) - ) { - // skip tags that have not ID because user not granted to create tags - continue; + // process if current user allowed to remove other's tag links + if ($this->aclManager->isResourceGranted(self::ACL_RESOURCE_REMOVE_ID_KEY)) { + // get 'not mine' taggings + $oldTags = $this->getTagging($resource, $this->getUser()->getId(), true); + $tagsToDelete = $oldTags->filter( + function ($tag) use ($newAllTags, $manager) { + return !$newAllTags->exists($manager->compareCallback($tag)); + } + ); + if (!$tagsToDelete->isEmpty()) { + $this->deleteTaggingByParams( + $tagsToDelete, + get_class($resource), + $resource->getTaggableId() + ); + } } - $this->em->persist($tag); + foreach ($tagsToAdd as $tag) { + if (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_ASSIGN_ID_KEY) + || (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_CREATE_ID_KEY) && !$tag->getId()) + ) { + // skip tags that have not ID because user not granted to create tags + continue; + } + + $this->em->persist($tag); - $alias = $this->mapper->getEntityConfig(get_class($resource)); + $alias = $this->mapper->getEntityConfig(get_class($resource)); - $tagging = $this->createTagging($tag, $resource) - ->setAlias($alias['alias']); + $tagging = $this->createTagging($tag, $resource) + ->setAlias($alias['alias']); - $this->em->persist($tagging); - } + $this->em->persist($tagging); + } - if (count($tagsToAdd)) { - $this->em->flush(); + if (!$tagsToAdd->isEmpty()) { + $this->em->flush(); + } } } + /** + * @param Tag $tag + * @return callable + */ + public function compareCallback($tag) + { + return function ($index, $item) use ($tag) { + /** @var Tag $item */ + return $item->getName() == $tag->getName(); + }; + } + /** * Loads all tags for the given taggable resource * @@ -299,7 +291,7 @@ public function loadTagging(Taggable $resource) /** * Remove tagging related to tags by params * - * @param array|int $tagIds + * @param array|ArrayCollection|int $tagIds * @param string $entityName * @param int $recordId * @param null|int $createdBy @@ -312,6 +304,14 @@ public function deleteTaggingByParams($tagIds, $entityName, $recordId, $createdB if (!$tagIds) { $tagIds = array(); + } elseif ($tagIds instanceof ArrayCollection) { + $tagIds = array_map( + function ($item) { + /** @var Tag $item */ + return $item->getId(); + }, + $tagIds->toArray() + ); } return $repository->deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy); @@ -346,7 +346,7 @@ private function createTagging(Tag $tag, Taggable $resource) * @param Taggable $resource Taggable resource * @param null|int $createdBy * @param bool $all - * @return array + * @return ArrayCollection */ private function getTagging(Taggable $resource, $createdBy = null, $all = false) { diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index 09b9c7047a3..d5a9dae6dbf 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -50,11 +50,10 @@ {% block oro_combobox_dataconfig_multi_autocomplete %} {{ block('oro_combobox_dataconfig_autocomplete') }} - select2Config.createSearchChoice = function(term, data) { if ( $(data).filter(function() { - return this.name.localeCompare(term) === 0; + return this.name.toLowerCase().localeCompare(term.toLowerCase()) === 0; }).length === 0 ) { {% if not resource_granted('oro_tag_create') %} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php index 11acc162a03..ee51da4192d 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php @@ -2,8 +2,10 @@ namespace Oro\Bundle\TagBundle\Tests\Unit\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Oro\Bundle\TagBundle\Entity\Tag; use Oro\Bundle\TagBundle\Entity\TagManager; +use Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Taggable; class TagManagerTest extends \PHPUnit_Framework_TestCase { @@ -15,6 +17,8 @@ class TagManagerTest extends \PHPUnit_Framework_TestCase const TEST_RECORD_ID = 1; const TEST_CREATED_ID = 22; + const TEST_USER_ID = 'someID'; + /** @var TagManager */ protected $manager; @@ -33,6 +37,9 @@ class TagManagerTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $router; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $user; + public function setUp() { $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') @@ -49,6 +56,20 @@ public function setUp() $this->router = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Routing\Router') ->disableOriginalConstructor()->getMock(); + $this->user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $this->user->expects($this->any()) + ->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($this->user)); + + $this->securityContext->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)); + $this->manager = new TagManager( $this->em, 'Oro\Bundle\TagBundle\Entity\Tag', @@ -124,12 +145,12 @@ public function getTagNames() /** * @dataProvider tagIdsProvider */ - public function testDeleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy) + public function testDeleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy, $expectedCallArg) { $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') ->disableOriginalConstructor()->getMock(); $repo->expects($this->once())->method('deleteTaggingByParams') - ->with(is_array($tagIds) ? $tagIds : array(), $entityName, $recordId, $createdBy); + ->with($expectedCallArg, $entityName, $recordId, $createdBy); $this->em->expects($this->once())->method('getRepository') ->will($this->returnValue($repo)); @@ -142,18 +163,32 @@ public function testDeleteTaggingByParams($tagIds, $entityName, $recordId, $crea */ public function tagIdsProvider() { + $tag = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag->expects($this->once())->method('getId') + ->will($this->returnValue(self::TEST_TAG_ID)); + return array( 'null value should pass as array' => array( - 'tagIds' => null, - 'entityName' => self::TEST_ENTITY_NAME, - 'recordId' => self::TEST_RECORD_ID, - 'createdBy' => self::TEST_CREATED_ID + 'tagIds' => null, + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array() ), 'some ids data ' => array( - 'tagIds' => array(self::TEST_TAG_ID), - 'entityName' => self::TEST_ENTITY_NAME, - 'recordId' => self::TEST_RECORD_ID, - 'createdBy' => self::TEST_CREATED_ID + 'tagIds' => array(self::TEST_TAG_ID), + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array(self::TEST_TAG_ID) + ), + 'some array collection' => array( + 'tagIds' => new ArrayCollection(array($tag)), + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array(self::TEST_TAG_ID) + ) ); } @@ -184,4 +219,140 @@ public function testLoadTagging() $this->manager->loadTagging($resource); } + + public function testCompareCallback() + { + $tag = new Tag('testName'); + $tagToCompare = new Tag('testName'); + $tagToCompare2 = new Tag('notTheSameName'); + + $callback = $this->manager->compareCallback($tag); + + $this->assertTrue($callback(1, $tagToCompare)); + $this->assertFalse($callback(1, $tagToCompare2)); + } + + public function testGetPreparedArrayFromDb() + { + $resource = new Taggable(array('id' => 1)); + $tagging = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); + + $tag1 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag1->expects($this->once())->method('getName') + ->will($this->returnValue('test name 1')); + $tag1->expects($this->any())->method('getId') + ->will($this->returnValue(1)); + $tag1->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $tag2 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag2->expects($this->once())->method('getName') + ->will($this->returnValue('test name 2')); + $tag2->expects($this->any())->method('getId') + ->will($this->returnValue(2)); + $tag2->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + $tagging->expects($this->exactly(2)) + ->method('getCreatedBy')->will($this->returnValue($userMock)); + $tagging->expects($this->any()) + ->method('getEntityName')->will($this->returnValue(get_class($resource))); + $tagging->expects($this->any()) + ->method('getRecordId')->will($this->returnValue(1)); + + $userMock->expects($this->at(0)) + ->method('getId')->will($this->returnValue(self::TEST_USER_ID)); + $userMock->expects($this->at(1))->method('getId') + ->will($this->returnValue('uniqueId2')); + + $this->user->expects($this->exactly(2))->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $this->router->expects($this->exactly(2)) + ->method('generate'); + + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->once())->method('getTagging')->with($resource, null, false) + ->will( + $this->returnValue( + array($tag1, $tag2) + ) + ); + + $this->em->expects($this->once())->method('getRepository')->with('Oro\Bundle\TagBundle\Entity\Tag') + ->will($this->returnValue($repo)); + + $result = $this->manager->getPreparedArray($resource); + + $this->assertCount(2, $result); + + $this->assertArrayHasKey('url', $result[0]); + $this->assertArrayHasKey('name', $result[0]); + $this->assertArrayHasKey('id', $result[0]); + $this->assertArrayHasKey('owner', $result[0]); + + $this->assertFalse($result[1]['owner']); + $this->assertTrue($result[0]['owner']); + } + + public function testGetPreparedArrayFromArray() + { + $resource = new Taggable(array('id' => 1)); + + $this->user->expects($this->exactly(2)) + ->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $this->router->expects($this->once()) + ->method('generate'); + + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->never())->method('getTagging'); + + $this->manager->getPreparedArray($resource, $this->tagForPreparing()); + } + + protected function tagForPreparing() + { + $tag1 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag2 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tagging = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); + + $tag1->expects($this->exactly(2)) + ->method('getName') + ->will($this->returnValue('test name 1')); + $tag1->expects($this->any()) + ->method('getId') + ->will($this->returnValue(null)); + $tag1->expects($this->exactly(1)) + ->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $tag2->expects($this->once())->method('getName') + ->will($this->returnValue('test name 2')); + $tag2->expects($this->any())->method('getId') + ->will($this->returnValue(2)); + $tag2->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + $tagging->expects($this->exactly(2)) + ->method('getCreatedBy')->will($this->returnValue($userMock)); + $tagging->expects($this->any()) + ->method('getEntityName')->will($this->returnValue('Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Taggable')); + $tagging->expects($this->any()) + ->method('getRecordId')->will($this->returnValue(1)); + + $userMock->expects($this->at(0)) + ->method('getId')->will($this->returnValue(self::TEST_USER_ID)); + $userMock->expects($this->at(1)) + ->method('getId')->will($this->returnValue('uniqueId2')); + + return new ArrayCollection(array($tag1, $tag2)); + } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Taggable.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Taggable.php new file mode 100644 index 00000000000..2aba8dc0099 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Taggable.php @@ -0,0 +1,71 @@ +tags = new ArrayCollection(); + + if (!isset($data['id'])) { + $data['id'] = time(); + } + + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function getTags() + { + return $this->tags; + } + + /** + * {@inheritdoc} + */ + public function setTags($tags) + { + $this->tags = $tags; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTaggableId() + { + return $this->data['id']; + } + + /** + * @param $name + * @return null + */ + public function __get($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . + ' in ' . $trace[0]['file'] . + ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + return null; + } +} From 0dcf40adada7cf4bd4847aaa2cd1ccc9d0986fce Mon Sep 17 00:00:00 2001 From: Aleksandr Smaga Date: Mon, 5 Aug 2013 18:30:47 +0200 Subject: [PATCH 105/541] BAP-1233: Wrong behavior on adding new tags during creating new entity - Fix unit test --- src/Oro/Bundle/UserBundle/Tests/Unit/Acl/AclInterceptorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Oro/Bundle/UserBundle/Tests/Unit/Acl/AclInterceptorTest.php b/src/Oro/Bundle/UserBundle/Tests/Unit/Acl/AclInterceptorTest.php index 20f8411f4b5..f3d403e2eaf 100644 --- a/src/Oro/Bundle/UserBundle/Tests/Unit/Acl/AclInterceptorTest.php +++ b/src/Oro/Bundle/UserBundle/Tests/Unit/Acl/AclInterceptorTest.php @@ -117,7 +117,7 @@ public function testNoAccessOnInternalRouteHtml() $this->requestAttributes->expects($this->once()) ->method('get') - ->will($this->returnValue('_internal')); + ->will($this->returnValue(null)); $this->aclManager ->expects($this->once()) From f8eec457cd57683b4a9a11a45f877e89b1048d92 Mon Sep 17 00:00:00 2001 From: Ivan Shakuta Date: Mon, 5 Aug 2013 16:43:25 +0000 Subject: [PATCH 106/541] BAP-1239: Functional Improvements for Grid - show/hide separate for each toolbar element: pageSize, actionPanel, pagination --- .../EmailBundle/Datagrid/EmailTemplateDatagridManager.php | 3 ++- .../Resources/public/js/app/datagrid/pagesize.js | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php index d68f914f7db..1fa73e3674c 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php @@ -221,12 +221,13 @@ public function getToolbarOptions() 10, 20, 50, 100, array('size' => 0, 'label' => $this->translate('oro.grid.page_size.all')) ), + 'enable' => false, ), 'pagination' => array( 'enable' => false, ), 'actionsPanel' => array( - 'enable' => false, + 'enable' => true, ) ); } diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js index a25d0052481..5515360cc0c 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js @@ -13,7 +13,7 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ '' + '
      ' + '' + '