diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 83cc87b82872d..6a2f7632c11f3 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Framework\App\Resource; /** @@ -45,6 +46,12 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract const ENTITY_TYPE_CODE = 'advanced_pricing'; + const VALIDATOR_MAIN = 'validator'; + + const VALIDATOR_WEBSITE = 'validator_website'; + + const VALIDATOR_GROUP_PRICE = 'validator_group_price'; + /** * Validation failure message template definitions * @@ -61,6 +68,31 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract ValidatorInterface::ERROR_INVALID_GROUP_PRICE_SITE => 'Group Price data website is invalid', ValidatorInterface::ERROR_INVALID_GROUP_PRICE_GROUP => 'Group Price customer group is invalid', ValidatorInterface::ERROR_GROUP_PRICE_DATA_INCOMPLETE => 'Group Price data is incomplete', + ValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL => + 'Value for \'%s\' attribute contains incorrect value, acceptable values are in decimal format', + ]; + + /** + * If we should check column names + * + * @var bool + */ + protected $needColumnCheck = true; + + /** + * Valid column names + * + * @array + */ + protected $validColumnNames = [ + self::COL_SKU, + self::COL_TIER_PRICE_WEBSITE, + self::COL_TIER_PRICE_CUSTOMER_GROUP, + self::COL_TIER_PRICE_QTY, + self::COL_TIER_PRICE, + self::COL_GROUP_PRICE_WEBSITE, + self::COL_GROUP_PRICE_CUSTOMER_GROUP, + self::COL_GROUP_PRICE, ]; /** @@ -96,9 +128,9 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract protected $_importProduct; /** - * @var AdvancedPricing\Validator + * @var array */ - protected $_validator; + protected $_validators = []; /** * @var array @@ -110,16 +142,6 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract */ protected $_oldSkus; - /** - * @var AdvancedPricing\Validator\Website - */ - protected $websiteValidator; - - /** - * @var AdvancedPricing\Validator\GroupPrice - */ - protected $groupPriceValidator; - /** * Permanent entity columns. * @@ -141,12 +163,15 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData - * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\ImportExport\Model\Resource\Import\Data $importData + * @param \Magento\Eav\Model\Config $config * @param \Magento\Framework\App\Resource $resource + * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceFactory $resourceFactory * @param \Magento\Catalog\Model\Product $productModel * @param \Magento\Catalog\Helper\Data $catalogData @@ -155,14 +180,18 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract * @param AdvancedPricing\Validator $validator * @param AdvancedPricing\Validator\Website $websiteValidator * @param AdvancedPricing\Validator\GroupPrice $groupPriceValidator + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Json\Helper\Data $jsonHelper, \Magento\ImportExport\Helper\Data $importExportData, - \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\ImportExport\Model\Resource\Import\Data $importData, + \Magento\Eav\Model\Config $config, \Magento\Framework\App\Resource $resource, + \Magento\ImportExport\Model\Resource\Helper $resourceHelper, + \Magento\Framework\Stdlib\StringUtils $string, + ProcessingErrorAggregatorInterface $errorAggregator, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceFactory $resourceFactory, \Magento\Catalog\Model\Product $productModel, \Magento\Catalog\Helper\Data $catalogData, @@ -177,17 +206,33 @@ public function __construct( $this->_importExportData = $importExportData; $this->_resourceHelper = $resourceHelper; $this->_dataSourceModel = $importData; - $this->_connection = $resource->getConnection(); + $this->_connection = $resource->getConnection('write'); $this->_resourceFactory = $resourceFactory; $this->_productModel = $productModel; $this->_catalogData = $catalogData; $this->_storeResolver = $storeResolver; $this->_importProduct = $importProduct; - $this->_validator = $validator; + $this->_validators[self::VALIDATOR_MAIN] = $validator->init($this); $this->_oldSkus = $this->retrieveOldSkus(); - $this->websiteValidator = $websiteValidator; - $this->groupPriceValidator = $groupPriceValidator; + $this->_validators[self::VALIDATOR_WEBSITE] = $websiteValidator; + $this->_validators[self::VALIDATOR_GROUP_PRICE] = $groupPriceValidator; + $this->errorAggregator = $errorAggregator; $this->_catalogProductEntity = $this->_resourceFactory->create()->getTable('catalog_product_entity'); + + foreach (array_merge($this->errorMessageTemplates, $this->_messageTemplates) as $errorCode => $message) { + $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); + } + } + + /** + * Validator object getter. + * + * @param string $type + * @return AdvancedPricing\Validator|AdvancedPricing\Validator\Website|AdvancedPricing\Validator\GroupPrice + */ + protected function _getValidator($type) + { + return $this->_validators[$type]; } /** @@ -211,7 +256,7 @@ public function validateRow(array $rowData, $rowNum) { $sku = false; if (isset($this->_validatedRows[$rowNum])) { - return !isset($this->_invalidRows[$rowNum]); + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } $this->_validatedRows[$rowNum] = true; // BEHAVIOR_DELETE use specific validation logic @@ -222,8 +267,8 @@ public function validateRow(array $rowData, $rowNum) } return true; } - if (!$this->_validator->isValid($rowData)) { - foreach ($this->_validator->getMessages() as $message) { + if (!$this->_getValidator(self::VALIDATOR_MAIN)->isValid($rowData)) { + foreach ($this->_getValidator(self::VALIDATOR_MAIN)->getMessages() as $message) { $this->addRowError($message, $rowNum); } } @@ -233,7 +278,7 @@ public function validateRow(array $rowData, $rowNum) if (false === $sku) { $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); } - return !isset($this->_invalidRows[$rowNum]); + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } /** @@ -277,10 +322,14 @@ public function deleteAdvancedPricing() $listSku = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { foreach ($bunch as $rowNum => $rowData) { - if ($this->validateRow($rowData, $rowNum)) { + $this->validateRow($rowData, $rowNum); + if (!$this->getErrorAggregator()->isRowInvalid($rowNum)) { $rowSku = $rowData[self::COL_SKU]; $listSku[] = $rowSku; } + if ($this->getErrorAggregator()->hasToBeTerminated()) { + $this->getErrorAggregator()->addRowToSkip($rowNum); + } } } if ($listSku) { @@ -307,6 +356,7 @@ public function replaceAdvancedPricing() * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function saveAndReplaceAdvancedPrices() { @@ -323,6 +373,11 @@ protected function saveAndReplaceAdvancedPrices() $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); continue; } + if ($this->getErrorAggregator()->hasToBeTerminated()) { + $this->getErrorAggregator()->addRowToSkip($rowNum); + continue; + } + $rowSku = $rowData[self::COL_SKU]; $listSku[] = $rowSku; if (!empty($rowData[self::COL_TIER_PRICE_WEBSITE])) { @@ -359,7 +414,7 @@ protected function saveAndReplaceAdvancedPrices() } } elseif (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { $this->processCountExistingPrices($tierPrices, self::TABLE_TIER_PRICE) - ->processcountExistingPrices($groupPrices, self::TABLE_GROUPED_PRICE) + ->processCountExistingPrices($groupPrices, self::TABLE_GROUPED_PRICE) ->processCountNewPrices($tierPrices, $groupPrices); $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE) ->saveProductPrices($groupPrices, self::TABLE_GROUPED_PRICE); @@ -462,7 +517,7 @@ protected function setUpdatedAt(array $listSku) */ protected function getWebSiteId($websiteCode) { - $result = $websiteCode == $this->websiteValidator->getAllWebsitesValue() || + $result = $websiteCode == $this->_getValidator(self::VALIDATOR_WEBSITE)->getAllWebsitesValue() || $this->_catalogData->isPriceGlobal() ? 0 : $this->_storeResolver->getWebsiteCodeToId($websiteCode); return $result; } @@ -475,7 +530,7 @@ protected function getWebSiteId($websiteCode) */ protected function getCustomerGroupId($customerGroup) { - $customerGroups = $this->groupPriceValidator->getCustomerGroups(); + $customerGroups = $this->_getValidator(self::VALIDATOR_GROUP_PRICE)->getCustomerGroups(); return $customerGroup == self::VALUE_ALL_GROUPS ? 0 : $customerGroups[$customerGroup]; } diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php index 71124546a93db..5ff3eff79bffd 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php @@ -43,14 +43,14 @@ public function isValid($value) } /** - * Init validators - * - * @return void + * @param \Magento\CatalogImportExport\Model\Import\Product $context + * @return $this */ - public function init() + public function init($context) { foreach ($this->validators as $validator) { - $validator->init(); + $validator->init($context); } + return $this; } } diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/GroupPrice.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/GroupPrice.php index 3d62cdc330c73..d3a59957e9e5e 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/GroupPrice.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/GroupPrice.php @@ -6,6 +6,7 @@ namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; +use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; class GroupPrice extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice { @@ -38,15 +39,42 @@ public function __construct( } /** - * Call parent init() - * - * @return $this + * {@inheritdoc} */ - public function init() + public function init($context) { foreach ($this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems() as $group) { $this->customerGroups[$group->getCode()] = $group->getId(); } + $this->context = $context; + } + + /** + * @param string $attribute + * @return void + */ + protected function addDecimalError($attribute) + { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate( + RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL + ), + $attribute + ) + ] + ); + } + + /** + * @return void + */ + protected function initCustomerGroups() + { + if (!$this->customerGroups) { + $this->init($this->context); + } } /** @@ -58,22 +86,27 @@ public function init() public function isValid($value) { $this->_clearMessages(); - if (!$this->customerGroups) { - $this->init(); + $this->initCustomerGroups(); + if (!$this->isValidValueAndLength($value)) { + return true; } - if ($this->isValidValueAndLength($value)) { - if (!isset($value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE]) - || !isset($value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP]) - || $this->hasEmptyColumns($value)) { - $this->_addMessages([self::ERROR_GROUP_PRICE_DATA_INCOMPLETE]); - return false; - } elseif ( - $value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP] == AdvancedPricing::VALUE_ALL_GROUPS - || !isset($this->customerGroups[$value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP]]) - ) { - $this->_addMessages([self::ERROR_INVALID_GROUP_PRICE_GROUP]); - return false; - } + if (!isset($value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE]) + || !isset($value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP]) + || $this->hasEmptyColumns($value)) { + $this->_addMessages([self::ERROR_GROUP_PRICE_DATA_INCOMPLETE]); + return false; + } elseif ( + $value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP] == AdvancedPricing::VALUE_ALL_GROUPS + || !isset($this->customerGroups[$value[AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP]]) + ) { + $this->_addMessages([self::ERROR_INVALID_GROUP_PRICE_GROUP]); + return false; + } + if (!is_numeric($value[AdvancedPricing::COL_GROUP_PRICE]) + || $value[AdvancedPricing::COL_GROUP_PRICE] < 0 + ) { + $this->addDecimalError(AdvancedPricing::COL_GROUP_PRICE); + return false; } return true; } @@ -86,7 +119,7 @@ public function isValid($value) public function getCustomerGroups() { if (!$this->customerGroups) { - $this->init(); + $this->init($this->context); } return $this->customerGroups; } diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php index 0ce8d991f7f95..03ffcc962f554 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php @@ -6,6 +6,7 @@ namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; +use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; class TierPrice extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice { @@ -39,15 +40,32 @@ public function __construct( } /** - * Call parent init() - * - * @return $this + * {@inheritdoc} */ - public function init() + public function init($context) { foreach ($this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems() as $group) { $this->customerGroups[$group->getCode()] = $group->getId(); } + $this->context = $context; + } + + /** + * @param string $attribute + * @return void + */ + protected function addDecimalError($attribute) + { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate( + RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL + ), + $attribute + ) + ] + ); } /** @@ -62,8 +80,9 @@ public function isValid($value) { $this->_clearMessages(); if (!$this->customerGroups) { - $this->init(); + $this->init($this->context); } + $valid = true; if ($this->isValidValueAndLength($value)) { if (!isset($value[AdvancedPricing::COL_TIER_PRICE_WEBSITE]) || !isset($value[AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP]) @@ -72,19 +91,27 @@ public function isValid($value) || $this->hasEmptyColumns($value) ) { $this->_addMessages([self::ERROR_TIER_DATA_INCOMPLETE]); - return false; + $valid = false; } elseif ($value[AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP] != AdvancedPricing::VALUE_ALL_GROUPS && !isset($this->customerGroups[$value[AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP]]) ) { $this->_addMessages([self::ERROR_INVALID_TIER_PRICE_GROUP]); - return false; - } elseif ($value[AdvancedPricing::COL_TIER_PRICE_QTY] <= 0 - || $value[AdvancedPricing::COL_TIER_PRICE] <= 0) { - $this->_addMessages([self::ERROR_INVALID_TIER_PRICE_QTY]); - return false; + $valid = false; + } + if ($valid) { + if (!is_numeric($value[AdvancedPricing::COL_TIER_PRICE_QTY]) + || $value[AdvancedPricing::COL_TIER_PRICE_QTY] < 0) { + $this->addDecimalError(AdvancedPricing::COL_TIER_PRICE_QTY); + $valid = false; + } + if (!is_numeric($value[AdvancedPricing::COL_TIER_PRICE]) + || $value[AdvancedPricing::COL_TIER_PRICE] < 0) { + $this->addDecimalError(AdvancedPricing::COL_TIER_PRICE); + $valid = false; + } } } - return true; + return $valid; } /** diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php index bbfb97222f6a3..322a5b1956816 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php @@ -6,10 +6,10 @@ namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; -use \Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; -class Website extends AbstractValidator implements RowValidatorInterface +class Website extends AbstractImportValidator implements RowValidatorInterface { /** * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver @@ -34,13 +34,29 @@ public function __construct( } /** - * Initialize validator + * {@inheritdoc} + */ + public function init($context) + { + return parent::init($context); + } + + /** + * Validate by website type * - * @return $this + * @param array $value + * @param string $websiteCode + * @return bool */ - public function init() + protected function isWebsiteValid($value, $websiteCode) { - return $this; + if (isset($value[$websiteCode]) && !empty($value[$websiteCode])) { + if ($value[$websiteCode] != $this->getAllWebsitesValue() + && !$this->storeResolver->getWebsiteCodeToId($value[$websiteCode])) { + return false; + } + } + return true; } /** @@ -52,18 +68,17 @@ public function init() public function isValid($value) { $this->_clearMessages(); - if ($value[AdvancedPricing::COL_TIER_PRICE_WEBSITE] != $this->getAllWebsitesValue() && - $value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE] != $this->getAllWebsitesValue()) { - if ((!empty($value[AdvancedPricing::COL_TIER_PRICE_WEBSITE]) - && !$this->storeResolver->getWebsiteCodeToId($value[AdvancedPricing::COL_TIER_PRICE_WEBSITE])) - || ((!empty($value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE])) - && !$this->storeResolver->getWebsiteCodeToId($value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE])) - ) { - $this->_addMessages([self::ERROR_INVALID_WEBSITE]); - return false; - } + $valid = true; + if (isset($value[AdvancedPricing::COL_TIER_PRICE]) && !empty($value[AdvancedPricing::COL_TIER_PRICE])) { + $valid *= $this->isWebsiteValid($value, AdvancedPricing::COL_TIER_PRICE_WEBSITE); } - return true; + if (isset($value[AdvancedPricing::COL_GROUP_PRICE]) && !empty($value[AdvancedPricing::COL_GROUP_PRICE])) { + $valid *= $this->isWebsiteValid($value, AdvancedPricing::COL_GROUP_PRICE_WEBSITE); + } + if (!$valid) { + $this->_addMessages([self::ERROR_INVALID_WEBSITE]); + } + return $valid; } /** diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/GroupPriceTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/GroupPriceTest.php index a3ec05281836b..c5acdc18a935d 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/GroupPriceTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/GroupPriceTest.php @@ -70,7 +70,7 @@ public function setUp() public function testInitInternalCalls() { $searchCriteria = $this->getMock('Magento\Framework\Api\SearchCriteria', [], [], '', false); - $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + $this->searchCriteriaBuilder->expects($this->any())->method('create')->willReturn($searchCriteria); $groupSearchResult = $this->getMockForAbstractClass( '\Magento\Customer\Api\Data\GroupSearchResultsInterface', [], @@ -78,7 +78,7 @@ public function testInitInternalCalls() false ); $this->groupRepository - ->expects($this->once()) + ->expects($this->any()) ->method('getList') ->with($searchCriteria) ->willReturn($groupSearchResult); @@ -88,17 +88,17 @@ public function testInitInternalCalls() ->setMethods(['getCode', 'getId']) ->getMockForAbstractClass(); $groupTest->expects($this->once())->method('getCode'); - $groupTest->expects($this->once())->method('getId'); + $groupTest->expects($this->any())->method('getId'); $groups = [$groupTest]; - $groupSearchResult->expects($this->once())->method('getItems')->willReturn($groups); + $groupSearchResult->expects($this->any())->method('getItems')->willReturn($groups); - $this->groupPrice->init(); + $this->groupPrice->init(null); } public function testInitAddToCustomerGroups() { $searchCriteria = $this->getMock('Magento\Framework\Api\SearchCriteria', [], [], '', false); - $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + $this->searchCriteriaBuilder->expects($this->any())->method('create')->willReturn($searchCriteria); $groupSearchResult = $this->getMockForAbstractClass( '\Magento\Customer\Api\Data\GroupSearchResultsInterface', [], @@ -106,7 +106,7 @@ public function testInitAddToCustomerGroups() false ); $this->groupRepository - ->expects($this->once()) + ->expects($this->any()) ->method('getList') ->with($searchCriteria) ->willReturn($groupSearchResult); @@ -122,11 +122,10 @@ public function testInitAddToCustomerGroups() $expectedCode => $expectedId, ]; $groupTest->expects($this->once())->method('getCode')->willReturn($expectedCode); - $groupTest->expects($this->once())->method('getId')->willReturn($expectedId); + $groupTest->expects($this->any())->method('getId')->willReturn($expectedId); $groups = [$groupTest]; - $groupSearchResult->expects($this->once())->method('getItems')->willReturn($groups); - - $this->groupPrice->init(); + $groupSearchResult->expects($this->any())->method('getItems')->willReturn($groups); + $this->groupPrice->init(null); $this->assertEquals($expectedCustomerGroups, $this->getPropertyValue($this->groupPrice, 'customerGroups')); } diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php index 313c038c2c900..a2d5480898e5d 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php @@ -71,7 +71,7 @@ public function setUp() public function testInitInternalCalls() { $searchCriteria = $this->getMock('Magento\Framework\Api\SearchCriteria', [], [], '', false); - $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + $this->searchCriteriaBuilder->expects($this->any())->method('create')->willReturn($searchCriteria); $groupSearchResult = $this->getMockForAbstractClass( '\Magento\Customer\Api\Data\GroupSearchResultsInterface', [], @@ -79,7 +79,7 @@ public function testInitInternalCalls() false ); $this->groupRepository - ->expects($this->once()) + ->expects($this->any()) ->method('getList') ->with($searchCriteria) ->willReturn($groupSearchResult); @@ -89,17 +89,17 @@ public function testInitInternalCalls() ->setMethods(['getCode', 'getId']) ->getMockForAbstractClass(); $groupTest->expects($this->once())->method('getCode'); - $groupTest->expects($this->once())->method('getId'); + $groupTest->expects($this->any())->method('getId'); $groups = [$groupTest]; - $groupSearchResult->expects($this->once())->method('getItems')->willReturn($groups); + $groupSearchResult->expects($this->any())->method('getItems')->willReturn($groups); - $this->tierPrice->init(); + $this->tierPrice->init(null); } public function testInitAddToCustomerGroups() { $searchCriteria = $this->getMock('Magento\Framework\Api\SearchCriteria', [], [], '', false); - $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + $this->searchCriteriaBuilder->expects($this->any())->method('create')->willReturn($searchCriteria); $groupSearchResult = $this->getMockForAbstractClass( '\Magento\Customer\Api\Data\GroupSearchResultsInterface', [], @@ -107,7 +107,7 @@ public function testInitAddToCustomerGroups() false ); $this->groupRepository - ->expects($this->once()) + ->expects($this->any()) ->method('getList') ->with($searchCriteria) ->willReturn($groupSearchResult); @@ -123,11 +123,12 @@ public function testInitAddToCustomerGroups() $expectedCode => $expectedId, ]; $groupTest->expects($this->once())->method('getCode')->willReturn($expectedCode); - $groupTest->expects($this->once())->method('getId')->willReturn($expectedId); + $groupTest->expects($this->any())->method('getId')->willReturn($expectedId); $groups = [$groupTest]; - $groupSearchResult->expects($this->once())->method('getItems')->willReturn($groups); + $groupSearchResult->expects($this->any())->method('getItems')->willReturn($groups); + + $this->tierPrice->init(null); - $this->tierPrice->init(); $this->assertEquals($expectedCustomerGroups, $this->getPropertyValue($this->tierPrice, 'customerGroups')); } @@ -159,11 +160,21 @@ public function testIsValidInitCall() */ public function testIsValidResultFalse($value, $hasEmptyColumns, $customerGroups) { - $this->tierPrice->expects($this->once())->method('isValidValueAndLength')->willReturn(true); - $this->tierPrice->expects($this->any())->method('hasEmptyColumns')->willReturn($hasEmptyColumns); - $this->setPropertyValue($this->tierPrice, 'customerGroups', $customerGroups); + $tierPrice = $this->getMock( + 'Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\GroupPrice', + ['init', '_clearMessages', 'isValidValueAndLength', 'hasEmptyColumns'], + [ + $this->groupRepository, + $this->searchCriteriaBuilder, + $this->storeResolver, + ], + '' + ); + $tierPrice->expects($this->once())->method('isValidValueAndLength')->willReturn(true); + $tierPrice->expects($this->any())->method('hasEmptyColumns')->willReturn($hasEmptyColumns); + $this->setPropertyValue($tierPrice, 'customerGroups', $customerGroups); - $result = $this->tierPrice->isValid($value); + $result = $tierPrice->isValid($value); $this->assertFalse($result); } @@ -186,11 +197,51 @@ public function testIsValidResultTrue() */ public function testIsValidAddMessagesCall($value, $hasEmptyColumns, $customerGroups, $expectedMessages) { + $priceContextMock = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Product', + [], + [ + '\Magento\Framework\Json\Helper\Data', + '\Magento\ImportExport\Helper\Data', + '\Magento\ImportExport\Model\Resource\Import\Data', + '\Magento\Eav\Model\Config', + '\Magento\Framework\App\Resource', + '\Magento\ImportExport\Model\Resource\Helper', + '\Magento\Framework\Stdlib\StringUtils', + 'ProcessingErrorAggregatorInterface', + ], + '', + false + ); + $this->tierPrice->expects($this->once())->method('isValidValueAndLength')->willReturn(true); $this->tierPrice->expects($this->any())->method('hasEmptyColumns')->willReturn($hasEmptyColumns); $this->setPropertyValue($this->tierPrice, 'customerGroups', $customerGroups); - $this->tierPrice->expects($this->once())->method('_addMessages')->with($expectedMessages); + $searchCriteria = $this->getMock('Magento\Framework\Api\SearchCriteria', [], [], '', false); + $this->searchCriteriaBuilder->expects($this->any())->method('create')->willReturn($searchCriteria); + $groupSearchResult = $this->getMockForAbstractClass( + '\Magento\Customer\Api\Data\GroupSearchResultsInterface', + [], + '', + false + ); + $this->groupRepository + ->expects($this->any()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($groupSearchResult); + + $groupTest = $this->getMockBuilder('\Magento\Customer\Api\Data\GroupInterface') + ->disableOriginalConstructor() + ->setMethods(['getCode', 'getId']) + ->getMockForAbstractClass(); + $groupTest->expects($this->once())->method('getCode'); + $groupTest->expects($this->any())->method('getId'); + $groups = [$groupTest]; + $groupSearchResult->expects($this->any())->method('getItems')->willReturn($groups); + + $this->tierPrice->init($priceContextMock); $this->tierPrice->isValid($value); } @@ -352,6 +403,7 @@ public function isValidAddMessagesCallDataProvider() * * @param object $object * @param string $property + * @return mixed */ protected function getPropertyValue($object, $property) { @@ -368,6 +420,7 @@ protected function getPropertyValue($object, $property) * @param object $object * @param string $property * @param mixed $value + * @return object */ protected function setPropertyValue(&$object, $property, $value) { diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php index 54045bbec6aa7..2d2c33661e257 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php @@ -55,7 +55,7 @@ public function setUp() public function testInit() { - $result = $this->website->init(); + $result = $this->website->init(null); $this->assertEquals($this->website, $result); } @@ -64,7 +64,7 @@ public function testInit() * @dataProvider isValidReturnDataProvider * * @param array $value - * @param string $allWebsitesValue + * @param string $allWebsites * @param string $colTierPriceWebsite * @param string $colGroupPriceWebsite * @param bool $expectedResult @@ -77,7 +77,7 @@ public function testIsValidReturn( $expectedResult ) { $this->website->expects($this->once())->method('_clearMessages'); - $this->website->expects($this->atLeastOnce())->method('getAllWebsitesValue')->willReturn($allWebsites); + $this->website->expects($this->any())->method('getAllWebsitesValue')->willReturn($allWebsites); $this->storeResolver->method('getWebsiteCodeToId')->willReturnMap([ [$value[AdvancedPricing::COL_TIER_PRICE_WEBSITE], $colTierPriceWebsite], [$value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE], $colGroupPriceWebsite], @@ -99,13 +99,13 @@ public function testIsValidReturnAddMessagesCall() $expectedMessages = [AdvancedPricing\Validator\Website::ERROR_INVALID_WEBSITE]; $this->website->expects($this->once())->method('_clearMessages'); - $this->website->expects($this->atLeastOnce())->method('getAllWebsitesValue')->willReturn($allWebsitesValue); + $this->website->expects($this->any())->method('getAllWebsitesValue')->willReturn($allWebsitesValue); $this->storeResolver->method('getWebsiteCodeToId')->willReturnMap([ [$value[AdvancedPricing::COL_TIER_PRICE_WEBSITE], $colTierPriceWebsite], [$value[AdvancedPricing::COL_GROUP_PRICE_WEBSITE], $colGroupPriceWebsite], ]); - $this->website->expects($this->once())->method('_addMessages')->with($expectedMessages); + $this->website->expects($this->any())->method('_addMessages')->with($expectedMessages); $this->website->isValid($value); } @@ -141,6 +141,8 @@ public function isValidReturnDataProvider() '$value' => [ AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier value', AdvancedPricing::COL_GROUP_PRICE_WEBSITE => 'group value', + AdvancedPricing::COL_TIER_PRICE => 'value', + AdvancedPricing::COL_GROUP_PRICE => 'group value', ], '$allWebsites' => 'not tier|group price website value', '$colTierPriceWebsite' => false, @@ -151,16 +153,20 @@ public function isValidReturnDataProvider() '$value' => [ AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier value', AdvancedPricing::COL_GROUP_PRICE_WEBSITE => 'group value', + AdvancedPricing::COL_TIER_PRICE => 'tier value', + AdvancedPricing::COL_GROUP_PRICE => 'value', ], '$allWebsites' => 'not tier|group price website value', - '$colTierPriceWebsite' => 'value', - '$colGroupPriceWebsite' => false, + '$colTierPriceWebsite' => false, + '$colGroupPriceWebsite' => 'value', '$expectedResult' => false, ], [ '$value' => [ AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier value', AdvancedPricing::COL_GROUP_PRICE_WEBSITE => 'group value', + AdvancedPricing::COL_TIER_PRICE => 'value', + AdvancedPricing::COL_GROUP_PRICE => ' group value', ], '$allWebsites' => 'not tier|group price website value', '$colTierPriceWebsite' => 'value', @@ -169,8 +175,10 @@ public function isValidReturnDataProvider() ], [ '$value' => [ - AdvancedPricing::COL_TIER_PRICE_WEBSITE => false, + AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier value', AdvancedPricing::COL_GROUP_PRICE_WEBSITE => 'group value', + AdvancedPricing::COL_TIER_PRICE => 'tier value', + AdvancedPricing::COL_GROUP_PRICE => 'value', ], '$allWebsites' => 'not tier|group price website value', '$colTierPriceWebsite' => 'value', diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php index 07033bb67525f..9c6a9b9713427 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php @@ -73,7 +73,7 @@ public function testInit() { $this->validatorTest->expects($this->once())->method('init'); - $this->validator->init(); + $this->validator->init(null); } public function isValidDataProvider() diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php index 473b5aa9114b4..4b000863e3738 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -13,7 +13,7 @@ /** * @SuppressWarnings(PHPMD) */ -class AdvancedPricingTest extends \PHPUnit_Framework_TestCase +class AdvancedPricingTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** @@ -26,21 +26,21 @@ class AdvancedPricingTest extends \PHPUnit_Framework_TestCase */ protected $catalogData; - /** - * @var \Magento\Catalog\Model\Product |\PHPUnit_Framework_MockObject_MockObject - */ - protected $productModel; - /** * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver |\PHPUnit_Framework_MockObject_MockObject */ protected $storeResolver; /** - * @var \Magento\CatalogImportExport\Model\Import\Product|PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogImportExport\Model\Import\Product|\PHPUnit_Framework_MockObject_MockObject */ protected $importProduct; + /** + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + */ + protected $productModel; + /** * @var AdvancedPricing\Validator |\PHPUnit_Framework_MockObject_MockObject */ @@ -71,6 +71,11 @@ class AdvancedPricingTest extends \PHPUnit_Framework_TestCase */ protected $dataSourceModel; + /** + * @var \Magento\Eav\Model\Config + */ + protected $eavConfig; + /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -106,8 +111,20 @@ class AdvancedPricingTest extends \PHPUnit_Framework_TestCase */ protected $advancedPricing; + /** + * @var \Magento\Framework\Stdlib\String + */ + protected $stringObject; + + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface + */ + protected $errorAggregator; + public function setUp() { + parent::setUp(); + $this->jsonHelper = $this->getMock( '\Magento\Framework\Json\Helper\Data', [], @@ -150,22 +167,31 @@ public function setUp() '', false ); - $this->resourceFactory = $this->getMock( - '\Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceFactory', - ['create', 'getTable'], + $this->eavConfig = $this->getMock( + '\Magento\Eav\Model\Config', + [], [], '', false ); - $this->resourceFactory->expects($this->any())->method('create')->willReturnSelf(); - $this->resourceFactory->expects($this->any())->method('getTable')->willReturnSelf(); - $this->productModel = $this->getMock( - '\Magento\Catalog\Model\Product', + $entityType = $this->getMock( + '\Magento\Eav\Model\Entity\Type', [], [], '', false ); + $entityType->method('getEntityTypeId')->willReturn(''); + $this->eavConfig->method('getEntityType')->willReturn($entityType); + $this->resourceFactory = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceFactory', + ['create', 'getTable'], + [], + '', + false + ); + $this->resourceFactory->expects($this->any())->method('create')->willReturnSelf(); + $this->resourceFactory->expects($this->any())->method('getTable')->willReturnSelf(); $this->catalogData = $this->getMock( '\Magento\Catalog\Helper\Data', [], @@ -187,6 +213,13 @@ public function setUp() '', false ); + $this->productModel = $this->getMock( + '\Magento\Catalog\Model\Product', + [], + [], + '', + false + ); $this->validator = $this->getMock( '\Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator', ['isValid', 'getMessages'], @@ -208,6 +241,14 @@ public function setUp() '', false ); + $this->stringObject = $this->getMock( + '\Magento\Framework\Stdlib\StringUtils', + [], + [], + '', + false + ); + $this->errorAggregator = $this->getErrorAggregatorObject(); $this->localeDate = $this->getMock( '\Magento\Framework\Stdlib\DateTime\Timezone', ['date', 'format'], @@ -250,7 +291,7 @@ public function testGetEntityTypeCode() * * @dataProvider validateRowResultDataProvider */ - public function testValidateRowResult($rowData, $validatedRows, $invalidRows, $behavior, $expectedResult) + public function testValidateRowResult($rowData, $behavior, $expectedResult) { $rowNum = 0; $advancedPricingMock = $this->getAdvancedPricingMock([ @@ -261,8 +302,6 @@ public function testValidateRowResult($rowData, $validatedRows, $invalidRows, $b 'getWebSiteId', 'getBehavior', ]); - $this->setPropertyValue($advancedPricingMock, '_validatedRows', $validatedRows); - $this->setPropertyValue($advancedPricingMock, '_invalidRows', $invalidRows); $this->validator->expects($this->any())->method('isValid')->willReturn(true); $advancedPricingMock->expects($this->any())->method('getBehavior')->willReturn($behavior); @@ -275,7 +314,7 @@ public function testValidateRowResult($rowData, $validatedRows, $invalidRows, $b * * @dataProvider validateRowAddRowErrorCallDataProvider */ - public function testValidateRowAddRowErrorCall($rowData, $validatedRows, $invalidRows, $behavior, $error) + public function testValidateRowAddRowErrorCall($rowData, $behavior, $error) { $rowNum = 0; $advancedPricingMock = $this->getAdvancedPricingMock([ @@ -286,8 +325,6 @@ public function testValidateRowAddRowErrorCall($rowData, $validatedRows, $invali 'getWebSiteId', 'getBehavior', ]); - $this->setPropertyValue($advancedPricingMock, '_validatedRows', $validatedRows); - $this->setPropertyValue($advancedPricingMock, '_invalidRows', $invalidRows); $this->validator->expects($this->any())->method('isValid')->willReturn(true); $advancedPricingMock->expects($this->any())->method('getBehavior')->willReturn($behavior); $advancedPricingMock->expects($this->once())->method('addRowError')->with($error, $rowNum); @@ -377,22 +414,19 @@ public function testSaveAndReplaceAdvancedPricesAppendBehaviourDataAndCalls( ->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND); $this->dataSourceModel->expects($this->at(0))->method('getNextBunch')->willReturn($data); - $this->advancedPricing->expects($this->once())->method('validateRow')->willReturn(true); + $this->advancedPricing->expects($this->any())->method('validateRow')->willReturn(true); - $this->advancedPricing->expects($this->atLeastOnce())->method('getCustomerGroupId')->willReturnMap([ + $this->advancedPricing->expects($this->any())->method('getCustomerGroupId')->willReturnMap([ [$data[0][AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP], $tierCustomerGroupId], [$data[0][AdvancedPricing::COL_GROUP_PRICE_CUSTOMER_GROUP], $groupCustomerGroupId] ]); - $this->advancedPricing->expects($this->atLeastOnce())->method('getWebSiteId')->willReturnMap([ + $this->advancedPricing->expects($this->any())->method('getWebSiteId')->willReturnMap([ [$data[0][AdvancedPricing::COL_TIER_PRICE_WEBSITE], $tierWebsiteId], [$data[0][AdvancedPricing::COL_GROUP_PRICE_WEBSITE], $groupWebsiteId] ]); - $this->advancedPricing->expects($this->exactly(2))->method('saveProductPrices')->withConsecutive( - [$expectedTierPrices, AdvancedPricing::TABLE_TIER_PRICE], - [$expectedGroupPrices, AdvancedPricing::TABLE_GROUPED_PRICE] - )->will($this->returnSelf()); + $this->advancedPricing->expects($this->any())->method('saveProductPrices')->will($this->returnSelf()); $this->advancedPricing->expects($this->any())->method('processCountExistingPrices')->willReturnSelf(); $this->advancedPricing->expects($this->any())->method('processCountNewPrices')->willReturnSelf(); @@ -432,7 +466,7 @@ public function testSaveAndReplaceAdvancedPricesReplaceBehaviourInternalCalls() ->method('getWebSiteId'); $this->advancedPricing - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('deleteProductTierAndGroupPrices') ->withConsecutive( [ @@ -447,7 +481,7 @@ public function testSaveAndReplaceAdvancedPricesReplaceBehaviourInternalCalls() ->willReturn(true); $this->advancedPricing - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('saveProductPrices') ->withConsecutive( [ @@ -692,23 +726,13 @@ public function validateRowResultDataProvider() '$rowData' => [ AdvancedPricing::COL_SKU => 'sku value', ], - '$validatedRows' => [ - 0 => ['value'] - ], - '$invalidRows' => [ - 0 => ['value'] - ], '$behavior' => null, - '$expectedResult' => false, + '$expectedResult' => true, ], [ '$rowData' => [ AdvancedPricing::COL_SKU => null, ], - '$validatedRows' => [], - '$invalidRows' => [ - 0 => ['value'] - ], '$behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE, '$expectedResult' => false, ], @@ -716,33 +740,9 @@ public function validateRowResultDataProvider() '$rowData' => [ AdvancedPricing::COL_SKU => 'sku value', ], - '$validatedRows' => [], - '$invalidRows' => [ - 0 => ['value'] - ], '$behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE, '$expectedResult' => true, - ], - [ - '$rowData' => [ - AdvancedPricing::COL_SKU => 'sku value', - ], - '$validatedRows' => [], - '$invalidRows' => [ - 0 => ['value'] - ], - '$behavior' => null, - '$expectedResult' => false, - ], - [ - '$rowData' => [ - AdvancedPricing::COL_SKU => 'sku value', - ], - '$validatedRows' => [], - '$invalidRows' => [], - '$behavior' => null, - '$expectedResult' => true, - ], + ] ]; } @@ -758,10 +758,6 @@ public function validateRowAddRowErrorCallDataProvider() '$rowData' => [ AdvancedPricing::COL_SKU => null, ], - '$validatedRows' => [], - '$invalidRows' => [ - 0 => ['value'] - ], '$behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE, '$error' => RowValidatorInterface::ERROR_SKU_IS_EMPTY, ], @@ -769,10 +765,6 @@ public function validateRowAddRowErrorCallDataProvider() '$rowData' => [ AdvancedPricing::COL_SKU => false, ], - '$validatedRows' => [], - '$invalidRows' => [ - 0 => ['value'] - ], '$behavior' => null, '$error' => RowValidatorInterface::ERROR_ROW_IS_ORPHAN, ], @@ -843,12 +835,15 @@ private function getAdvancedPricingMock($methods = []) '\Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing', $methods, [ - $this->localeDate, $this->jsonHelper, $this->importExportData, - $this->resourceHelper, $this->dataSourceModel, + $this->eavConfig, $this->resource, + $this->resourceHelper, + $this->stringObject, + $this->errorAggregator, + $this->localeDate, $this->resourceFactory, $this->productModel, $this->catalogData, @@ -856,7 +851,7 @@ private function getAdvancedPricingMock($methods = []) $this->importProduct, $this->validator, $this->websiteValidator, - $this->groupPriceValidator, + $this->groupPriceValidator ], '' ); diff --git a/app/code/Magento/BundleImportExport/Model/Export/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Export/Product/Type/Bundle.php new file mode 100644 index 0000000000000..8c68287283146 --- /dev/null +++ b/app/code/Magento/BundleImportExport/Model/Export/Product/Type/Bundle.php @@ -0,0 +1,13 @@ + self::VALUE_DYNAMIC, + '1' => self::VALUE_FIXED + ]; + + /** + * Mapping for price views + * + * @var array + */ + protected $priceViewMapping = [ + '0' => self::VALUE_PRICE_RANGE, + '1' => self::VALUE_AS_LOW_AS + ]; + + /** + * Mapping for price types + * + * @var array + */ + protected $priceTypeMapping = [ + '0' => self::VALUE_FIXED, + '1' => self::VALUE_PERCENT + ]; + + /** + * Bundle product columns + * + * @var array + */ + protected $bundleColumns = [ + self::BUNDLE_PRICE_TYPE_COL, + self::BUNDLE_SKU_TYPE_COL, + self::BUNDLE_PRICE_VIEW_COL, + self::BUNDLE_WEIGHT_TYPE_COL, + self::BUNDLE_VALUES_COL + ]; + + /** + * Product's bundle data + * + * @var array + */ + protected $bundleData = []; + + /** + * Prepare data for export + * + * @param \Magento\Catalog\Model\Resource\Product\Collection $collection + * @param int $productIds + * @return $this + */ + public function prepareData($collection, $productIds) + { + $collection->addAttributeToFilter( + 'entity_id', + ['in' => $productIds] + )->addAttributeToFilter( + 'type_id', + ['eq' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE] + ); + + return $this->populateBundleData($collection); + } + + /** + * Set headers columns + * + * @param array $columns + * @return array + */ + public function addHeaderColumns($columns) + { + $columns = array_merge($columns, $this->bundleColumns); + + return $columns; + } + + /** + * Add data for export + * + * @param array $dataRow + * @param int $productId + * @return array + */ + public function addData($dataRow, $productId) + { + if (!empty($this->bundleData[$productId])) { + $dataRow = array_merge($this->cleanNotBundleAdditionalAttributes($dataRow), $this->bundleData[$productId]); + } + + return $dataRow; + } + + /** + * Calculate the largest links block + * + * @param array $additionalRowsCount + * @param int $productId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getAdditionalRowsCount($additionalRowsCount, $productId) + { + return $additionalRowsCount; + } + + /** + * Populate bundle product data + * + * @param \Magento\Catalog\Model\Resource\Product\Collection $collection + * @return $this + */ + protected function populateBundleData($collection) + { + foreach ($collection as $product) { + $id = $product->getId(); + $this->bundleData[$id][self::BUNDLE_PRICE_TYPE_COL] = $this->getTypeValue($product->getPriceType()); + $this->bundleData[$id][self::BUNDLE_SKU_TYPE_COL] = $this->getTypeValue($product->getSkuType()); + $this->bundleData[$id][self::BUNDLE_PRICE_VIEW_COL] = $this->getPriceViewValue($product->getPriceView()); + $this->bundleData[$id][self::BUNDLE_WEIGHT_TYPE_COL] = $this->getTypeValue($product->getWeightType()); + $this->bundleData[$id][self::BUNDLE_VALUES_COL] = $this->getFormattedBundleOptionValues($product); + } + + return $this; + } + + /** + * Retrieve formatted bundle options + * + * @param \Magento\Catalog\Model\Product $product + * @return string + */ + protected function getFormattedBundleOptionValues($product) + { + /** @var \Magento\Bundle\Model\Resource\Option\Collection $optionsCollection */ + $optionsCollection = $product->getTypeInstance() + ->getOptionsCollection($product) + ->setOrder('position', Collection::SORT_ORDER_ASC); + + $bundleData = ''; + foreach ($optionsCollection as $option) { + $bundleData .= $this->getFormattedBundleSelections( + $this->getFormattedOptionValues($option), + $product->getTypeInstance() + ->getSelectionsCollection([$option->getId()], $product) + ->setOrder('position', Collection::SORT_ORDER_ASC) + ); + } + + return rtrim($bundleData, ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR); + } + + /** + * Retrieve formatted bundle selections + * + * @param string $optionValues + * @param SelectionCollection $selections + * @return string + */ + protected function getFormattedBundleSelections($optionValues, SelectionCollection $selections) + { + $bundleData = ''; + $selections->addAttributeToSort('position'); + foreach ($selections as $selection) { + $selectionData = [ + 'sku' => $selection->getSku(), + 'price' => $selection->getSelectionPriceValue(), + 'default' => $selection->getIsDefault(), + 'default_qty' => $selection->getSelectionQty(), + 'price_type' => $this->getPriceTypeValue($selection->getSelectionPriceType()) + ]; + $bundleData .= $optionValues + . ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode( + ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + array_map( + function ($value, $key) { + return $key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $value; + }, + $selectionData, + array_keys($selectionData) + ) + ) + . ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR; + } + + return $bundleData; + } + + /** + * Retrieve option value of bundle product + * + * @param \Magento\Bundle\Model\Option $option + * @return string + */ + protected function getFormattedOptionValues($option) + { + return 'name' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getTitle() . ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getType() . ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getRequired(); + } + + /** + * Retrieve bundle type value by code + * + * @param string $type + * @return string + */ + protected function getTypeValue($type) + { + return isset($this->typeMapping[$type]) ? $this->typeMapping[$type] : self::VALUE_DYNAMIC; + } + + /** + * Retrieve bundle price view value by code + * + * @param string $type + * @return string + */ + protected function getPriceViewValue($type) + { + return isset($this->priceViewMapping[$type]) ? $this->priceViewMapping[$type] : self::VALUE_PRICE_RANGE; + } + + /** + * Retrieve bundle price type value by code + * + * @param string $type + * @return string + */ + protected function getPriceTypeValue($type) + { + return isset($this->priceTypeMapping[$type]) ? $this->priceTypeMapping[$type] : null; + } + + /** + * Remove bundle specified additional attributes as now they are stored in specified columns + * + * @param array $dataRow + * @return array + */ + protected function cleanNotBundleAdditionalAttributes($dataRow) + { + if (!empty($dataRow['additional_attributes'])) { + $additionalAttributes = explode( + ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + $dataRow['additional_attributes'] + ); + $dataRow['additional_attributes'] = $this->getNotBundleAttributes($additionalAttributes); + } + + return $dataRow; + } + + /** + * Retrieve not bundle additional attributes + * + * @param array $additionalAttributes + * @return string + */ + protected function getNotBundleAttributes($additionalAttributes) + { + $cleanedAdditionalAttributes = ''; + foreach ($additionalAttributes as $attribute) { + list($attributeCode, $attributeValue) = explode(ImportProductModel::PAIR_NAME_VALUE_SEPARATOR, $attribute); + if (!in_array('bundle_' . $attributeCode, $this->bundleColumns)) { + $cleanedAdditionalAttributes .= $attributeCode + . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $attributeValue + . ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR; + } + } + + return rtrim($cleanedAdditionalAttributes, ImportProductModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR); + } +} diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index 7dab7fa3d8126..3d90cfc916fab 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -608,6 +608,23 @@ protected function insertSelections() return $this; } + /** + * Initialize attributes parameters for all attributes' sets. + * + * @return $this + */ + protected function _initAttributes() + { + parent::_initAttributes(); + if (isset(self::$attributeCodeToId['price_type']) && $id = self::$attributeCodeToId['price_type']) { + self::$commonAttributesCache[$id]['type'] = 'select'; + self::$commonAttributesCache[$id]['options'] = [ + self::VALUE_DYNAMIC => BundlePrice::PRICE_TYPE_DYNAMIC, + self::VALUE_FIXED => BundlePrice::PRICE_TYPE_FIXED, + ]; + } + } + /** * Delete options and selections. * diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php new file mode 100644 index 0000000000000..c01e20f7f43a4 --- /dev/null +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -0,0 +1,203 @@ +objectManagerHelper = new ObjectManagerHelper($this); + $this->rowCustomizerMock = $this->objectManagerHelper->getObject( + '\Magento\BundleImportExport\Model\Export\RowCustomizer' + ); + $this->productResourceCollection = $this->getMock( + '\Magento\Catalog\Model\Resource\Product\Collection', + ['addAttributeToFilter', 'getIterator'], + [], + '', + false + ); + $this->product = $this->getMock( + '\Magento\Catalog\Model\Product', + [ + 'getId', + 'getPriceType', + 'getSkuType', + 'getPriceView', + 'getWeightType', + 'getTypeInstance', + 'getOptionsCollection', + 'getSelectionsCollection' + ], + [], + '', + false + ); + $this->product->expects($this->any())->method('getId')->willReturn(1); + $this->product->expects($this->any())->method('getPriceType')->willReturn(1); + $this->product->expects($this->any())->method('getSkuType')->willReturn(1); + $this->product->expects($this->any())->method('getPriceView')->willReturn(1); + $this->product->expects($this->any())->method('getWeightType')->willReturn(1); + $this->product->expects($this->any())->method('getTypeInstance')->willReturnSelf(); + $this->optionsCollection = $this->getMock( + '\Magento\Bundle\Model\Resource\Option\Collection', + ['setOrder', 'getIterator'], + [], + '', + false + ); + $this->product->expects($this->any())->method('getOptionsCollection')->willReturn($this->optionsCollection); + $this->optionsCollection->expects($this->any())->method('setOrder')->willReturnSelf(); + $this->option = $this->getMock( + '\Magento\Bundle\Model\Option', + ['getId', 'getTitle', 'getType', 'getRequired'], + [], + '', + false + ); + $this->option->expects($this->any())->method('getId')->willReturn(1); + $this->option->expects($this->any())->method('getTitle')->willReturn('title'); + $this->option->expects($this->any())->method('getType')->willReturn(1); + $this->option->expects($this->any())->method('getRequired')->willReturn(1); + $this->optionsCollection->expects($this->any())->method('getIterator')->will( + $this->returnValue(new \ArrayIterator([$this->option])) + ); + $this->selection = $this->getMock( + '\Magento\Catalog\Model\Product', + ['getSku', 'getSelectionPriceValue', 'getIsDefault', 'getSelectionQty', 'getSelectionPriceType'], + [], + '', + false + ); + $this->selection->expects($this->any())->method('getSku')->willReturn(1); + $this->selection->expects($this->any())->method('getSelectionPriceValue')->willReturn(1); + $this->selection->expects($this->any())->method('getSelectionQty')->willReturn(1); + $this->selection->expects($this->any())->method('getSelectionPriceType')->willReturn(1); + $this->selectionsCollection = $this->getMock( + '\Magento\Bundle\Model\Resource\Selection\Collection', + ['getIterator', 'addAttributeToSort'], + [], + '', + false + ); + $this->selectionsCollection->expects($this->any())->method('getIterator')->will( + $this->returnValue(new \ArrayIterator([$this->selection])) + ); + $this->selectionsCollection->expects($this->any())->method('addAttributeToSort')->willReturnSelf(); + $this->product->expects($this->any())->method('getSelectionsCollection')->willReturn( + $this->selectionsCollection + ); + $this->productResourceCollection->expects($this->any())->method('addAttributeToFilter')->willReturnSelf(); + $this->productResourceCollection->expects($this->any())->method('getIterator')->will( + $this->returnValue(new \ArrayIterator([$this->product])) + ); + } + + /** + * Test prepareData() + */ + public function testPrepareData() + { + $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); + } + + /** + * Test addHeaderColumns() + */ + public function testAddHeaderColumns() + { + $productData = [0 => 'sku']; + $expectedData = [ + 0 => 'sku', + 1 => 'bundle_price_type', + 2 => 'bundle_sku_type', + 3 => 'bundle_price_view', + 4 => 'bundle_weight_type', + 5 => 'bundle_values' + ]; + $this->assertEquals($expectedData, $this->rowCustomizerMock->addHeaderColumns($productData)); + } + + /** + * Test addData() + */ + public function testAddData() + { + $preparedData = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); + $dataRow = [ + 'sku' => 'sku1', + 'additional_attributes' => 'attribute=1,sku_type=1,price_type=1,price_view=1,weight_type=1,values=values' + ]; + $preparedRow = $preparedData->addData($dataRow, 1); + $expected = [ + 'sku' => 'sku1', + 'additional_attributes' => 'attribute=1', + 'bundle_price_type' => 'fixed', + 'bundle_sku_type' => 'fixed', + 'bundle_price_view' => 'As low as', + 'bundle_weight_type' => 'fixed', + 'bundle_values' => 'name=title,type=1,required=1,sku=1,price=1,default=,default_qty=1,price_type=percent' + ]; + $this->assertEquals($expected, $preparedRow); + } + + /** + * Test getAdditionalRowsCount() + */ + public function testGetAdditionalRowsCount() + { + $count = [5]; + $this->assertEquals($count, $this->rowCustomizerMock->getAdditionalRowsCount($count, 0)); + } +} diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php index 2f3a075f1c503..1841d2fc7f1f2 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php @@ -6,9 +6,7 @@ namespace Magento\BundleImportExport\Test\Unit\Model\Import\Product\Type; -use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -class BundleTest extends \PHPUnit_Framework_TestCase +class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * @var \Magento\BundleImportExport\Model\Import\Product\Type\Bundle @@ -16,14 +14,14 @@ class BundleTest extends \PHPUnit_Framework_TestCase protected $bundle; /** - * @var ObjectManagerHelper + * @var \Magento\Framework\App\Resource|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerHelper; + protected $resource; /** - * @var \Magento\Framework\App\Resource|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject */ - protected $resource; + protected $select; /** * @var \Magento\CatalogImportExport\Model\Import\Product|\PHPUnit_Framework_MockObject_MockObject @@ -54,15 +52,72 @@ class BundleTest extends \PHPUnit_Framework_TestCase */ protected $setCollection; + /** + * + * @return void + */ + protected function initFetchAllCalls() + { + $fetchAllForInitAttributes = [ + [ + 'attribute_set_name' => '1', + 'attribute_id' => '1', + ], + [ + 'attribute_set_name' => '2', + 'attribute_id' => '2', + ], + ]; + + $fetchAllForOtherCalls = [[ + 'selection_id' => '1', + 'option_id' => '1', + 'parent_product_id' => '1', + 'product_id' => '1', + 'position' => '1', + 'is_default' => '1' + ]]; + + $this->connection + ->method('fetchAll') + ->with($this->select) + ->will($this->onConsecutiveCalls( + $fetchAllForInitAttributes, + $fetchAllForOtherCalls, + $fetchAllForInitAttributes, + $fetchAllForOtherCalls, + $fetchAllForInitAttributes, + $fetchAllForOtherCalls, + $fetchAllForInitAttributes, + $fetchAllForOtherCalls, + $fetchAllForInitAttributes, + $fetchAllForInitAttributes, + $fetchAllForInitAttributes, + $fetchAllForInitAttributes, + $fetchAllForInitAttributes + )); + } + protected function setUp() { + parent::setUp(); + $this->entityModel = $this->getMock( 'Magento\CatalogImportExport\Model\Import\Product', - ['getBehavior', 'getNewSku', 'getNextBunch', 'isRowAllowedToImport', 'getRowScope', 'getConnection'], + [ + 'getErrorAggregator', + 'getBehavior', + 'getNewSku', + 'getNextBunch', + 'isRowAllowedToImport', + 'getRowScope', + 'getConnection' + ], [], '', false ); + $this->entityModel->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $this->connection = $this->getMock( 'Magento\Framework\DB\Adapter\Pdo\Mysql', ['select', 'fetchAll', 'fetchPairs', 'joinLeft', 'insertOnDuplicate', 'delete', 'quoteInto', 'fetchAssoc'], @@ -70,15 +125,13 @@ protected function setUp() '', false ); - $select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); - $select->expects($this->any())->method('from')->will($this->returnSelf()); - $select->expects($this->any())->method('where')->will($this->returnSelf()); - $select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); - $connection = $this->getMock('Magento\Framework\DB\Adapter\Pdo\Mysql', [], [], '', false); - $connection->expects($this->any())->method('quoteInto')->will($this->returnValue('query')); - $select->expects($this->any())->method('getConnection')->willReturn($connection); - $this->connection->expects($this->any())->method('select')->will($this->returnValue($select)); - $this->connection->expects($this->any())->method('fetchAll')->will($this->returnValue([])); + $this->select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); + $this->select->expects($this->any())->method('from')->will($this->returnSelf()); + $this->select->expects($this->any())->method('where')->will($this->returnSelf()); + $this->select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); + $this->select->expects($this->any())->method('getConnection')->willReturn($this->connection); + $this->connection->expects($this->any())->method('select')->will($this->returnValue($this->select)); + $this->initFetchAllCalls(); $this->connection->expects($this->any())->method('insertOnDuplicate')->willReturnSelf(); $this->connection->expects($this->any())->method('delete')->willReturnSelf(); $this->connection->expects($this->any())->method('quoteInto')->willReturn(''); @@ -89,12 +142,8 @@ protected function setUp() '', false ); - $this->resource->expects($this->any())->method('getConnection')->will( - $this->returnValue($this->connection) - ); - $this->resource->expects($this->any())->method('getTableName')->will( - $this->returnValue('tableName') - ); + $this->resource->expects($this->any())->method('getConnection')->will($this->returnValue($this->connection)); + $this->resource->expects($this->any())->method('getTableName')->will($this->returnValue('tableName')); $this->attrSetColFac = $this->getMock( 'Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory', ['create'], @@ -125,15 +174,11 @@ protected function setUp() $attrCollection = $this->getMock('\Magento\Catalog\Model\Resource\Product\Attribute\Collection', [], [], '', false); $attrCollection->expects($this->any())->method('addFieldToFilter')->willReturn([]); - - $this->prodAttrColFac->expects($this->any())->method('create')->will( - $this->returnValue($attrCollection) - ); + $this->prodAttrColFac->expects($this->any())->method('create')->will($this->returnValue($attrCollection)); $this->params = [ 0 => $this->entityModel, 1 => 'bundle' ]; - $this->objectManagerHelper = new ObjectManagerHelper($this); $this->bundle = $this->objectManagerHelper->getObject( 'Magento\BundleImportExport\Model\Import\Product\Type\Bundle', @@ -165,17 +210,7 @@ public function testSaveData($skus, $bunch, $allowImport) $allowImport )); - $connection = $this->getMock('Magento\Framework\DB\Adapter\Pdo\Mysql', [], [], '', false); - $connection->expects($this->any())->method('quoteInto')->will($this->returnValue('query')); - - $select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); - $select->expects($this->any())->method('getConnection')->willReturn($connection); - $select->expects($this->any())->method('from')->will($this->returnSelf()); - $select->expects($this->any())->method('where')->will($this->returnSelf()); - $select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); - $this->connection->expects($this->any())->method('select')->will($this->returnValue($select)); - - $this->connection->expects($this->any())->method('fetchAssoc')->with($select)->will($this->returnValue([ + $this->connection->expects($this->any())->method('fetchAssoc')->with($this->select)->will($this->returnValue([ '1' => [ 'option_id' => '1', 'parent_id' => '1', @@ -220,15 +255,6 @@ public function testSaveData($skus, $bunch, $allowImport) ] ] ])); - $this->connection->expects($this->any())->method('fetchAll')->with($select)->will($this->returnValue([[ - 'selection_id' => '1', - 'option_id' => '1', - 'parent_product_id' => '1', - 'product_id' => '1', - 'position' => '1', - 'is_default' => '1' - ]])); - $this->bundle->saveData(); } diff --git a/app/code/Magento/BundleImportExport/etc/di.xml b/app/code/Magento/BundleImportExport/etc/di.xml index ba3b3261d7850..f91245bf6ad75 100644 --- a/app/code/Magento/BundleImportExport/etc/di.xml +++ b/app/code/Magento/BundleImportExport/etc/di.xml @@ -6,4 +6,11 @@ */ --> + + + + Magento\BundleImportExport\Model\Export\RowCustomizer + + + diff --git a/app/code/Magento/BundleImportExport/etc/export.xml b/app/code/Magento/BundleImportExport/etc/export.xml new file mode 100644 index 0000000000000..53950ed3b4210 --- /dev/null +++ b/app/code/Magento/BundleImportExport/etc/export.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 3b0a02485756b..0377e0ca63827 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -879,8 +879,15 @@ protected function collectRawData() ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $attrValue; } $data[$itemId][$storeId][$fieldName] = $attrValue; - } else { - $this->collectMultiselectValues($item, $code, $storeId); + } + } else { + $this->collectMultiselectValues($item, $code, $storeId); + if (!empty($this->collectedMultiselectsData[$storeId][$itemId][$code])) { + $additionalAttributes[$code] = $fieldName . + ImportProduct::PAIR_NAME_VALUE_SEPARATOR . implode( + ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, + $this->collectedMultiselectsData[$storeId][$itemId][$code] + ); } } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index a2f65405c69ab..dc2fefaa9f83e 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -13,6 +13,8 @@ use Magento\Framework\Model\Resource\Db\TransactionManagerInterface; use Magento\Framework\Model\Resource\Db\ObjectRelationProcessor; use Magento\Framework\Stdlib\DateTime; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Import entity product model @@ -225,6 +227,15 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found', ValidatorInterface::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found', ValidatorInterface::ERROR_MEDIA_DATA_INCOMPLETE => 'Media data is incomplete', + ValidatorInterface::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length', + ValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value, acceptable values are in %s format', + ValidatorInterface::ERROR_ABSENT_REQUIRED_ATTRIBUTE => 'Attribute %s is required', + ValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION => 'Value for \'%s\' attribute contains incorrect value, see acceptable values on settings specified for Admin', + ValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE => 'Duplicated unique attribute', + ValidatorInterface::ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS => 'Value for \'%s\' sub attribute in \'%s\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\', \'radio\', \'text\'', + ValidatorInterface::ERROR_INVALID_MEDIA_URL_OR_PATH => 'Wrong URL/path used for attribute %s', + ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage', + ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', ]; @@ -429,6 +440,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ protected $_resourceFactory; + /** + * @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\Resource + */ + protected $_resource; + /** * @var \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory */ @@ -561,6 +577,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param \Magento\Framework\App\Resource $resource * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\Stdlib\StringUtils $string + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration @@ -576,17 +593,20 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param UploaderFactory $uploaderFactory * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\CatalogInventory\Model\Resource\Stock\ItemFactory $stockResItemFac - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param DateTime\TimezoneInterface $localeDate * @param DateTime $dateTime * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry * @param Product\StoreResolver $storeResolver * @param Product\SkuProcessor $skuProcessor * @param Product\CategoryProcessor $categoryProcessor - * @param Product\TaxClassProcessor $taxClassProcessor * @param Product\Validator $validator + * @param ObjectRelationProcessor $objectRelationProcessor + * @param TransactionManagerInterface $transactionManager + * @param Product\TaxClassProcessor $taxClassProcessor * @param array $data * @throws \Magento\Framework\Exception\LocalizedException + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -597,6 +617,7 @@ public function __construct( \Magento\Framework\App\Resource $resource, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\Stdlib\StringUtils $string, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, @@ -650,7 +671,16 @@ public function __construct( $this->objectRelationProcessor = $objectRelationProcessor; $this->transactionManager = $transactionManager; $this->taxClassProcessor = $taxClassProcessor; - parent::__construct($jsonHelper, $importExportData, $importData, $config, $resource, $resourceHelper, $string); + parent::__construct( + $jsonHelper, + $importExportData, + $importData, + $config, + $resource, + $resourceHelper, + $string, + $errorAggregator + ); $this->_optionEntity = isset( $data['option_entity'] ) ? $data['option_entity'] : $optionFactory->create( @@ -660,7 +690,7 @@ public function __construct( $this->_initAttributeSets() ->_initTypeModels() ->_initSkus(); - $this->validator->init(); + $this->validator->init($this); } /** @@ -670,53 +700,17 @@ public function __construct( * @param array $attrParams Attribute params * @param array $rowData Row data * @param int $rowNum - * - * @return boolean - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @return bool */ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum) { - switch ($attrParams['type']) { - case 'varchar': - $val = $this->string->cleanString($rowData[$attrCode]); - $valid = $this->string->strlen($val) < self::DB_MAX_VARCHAR_LENGTH; - break; - case 'decimal': - $val = trim($rowData[$attrCode]); - $valid = is_numeric($val); - break; - case 'select': - case 'multiselect': - $valid = isset($attrParams['options'][strtolower($rowData[$attrCode])]); - break; - case 'int': - $val = trim($rowData[$attrCode]); - $valid = (string)(int)$val === $val; - break; - case 'datetime': - $val = trim($rowData[$attrCode]); - $valid = strtotime($val) !== false; - break; - case 'text': - $val = $this->string->cleanString($rowData[$attrCode]); - $valid = $this->string->strlen($val) < self::DB_MAX_TEXT_LENGTH; - break; - default: - $valid = true; - break; - } - - if (!$valid) { - $this->addRowError(__("Please correct the value for '%s'."), $rowNum, $attrCode); - } elseif (!empty($attrParams['is_unique'])) { - if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]]) && ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[self::COL_SKU])) { - $this->addRowError(__("Duplicate Unique Attribute for '%s'"), $rowNum, $attrCode); - return false; + if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { + foreach ($this->validator->getMessages() as $message) { + $this->addRowError($message, $rowNum, $attrCode); } - $this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] = $rowData[self::COL_SKU]; + return false; } - return (bool)$valid; + return true; } /** @@ -757,6 +751,18 @@ public function getMediaGalleryAttributeId() return $this->_mediaGalleryAttributeId; } + /** + * @param string $name + * @return mixed + */ + public function retrieveProductTypeByName($name) + { + if (isset($this->_productTypeModels[$name])) { + return $this->_productTypeModels[$name]; + } + return null; + } + /** * Set import parameters * @@ -940,12 +946,23 @@ protected function _initTypeModels() $this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping()); $this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes()); } + $this->_initErrorTemplates(); // remove doubles $this->_specialAttributes = array_unique($this->_specialAttributes); return $this; } + /** + * Initialize Product error templates + */ + protected function _initErrorTemplates() + { + foreach ($this->_messageTemplates as $errorCode => $template) { + $this->addMessageTemplate($errorCode, $template); + } + } + /** * Set valid attribute set and product type to rows with all scopes * to ensure that existing products doesn't changed. @@ -1295,6 +1312,7 @@ protected function _prepareAllMediaFiles($allImagesFromBunch) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _saveProducts() { @@ -1323,13 +1341,16 @@ protected function _saveProducts() if (!$this->validateRow($rowData, $rowNum)) { continue; } + if ($this->getErrorAggregator()->hasToBeTerminated()) { + $this->getErrorAggregator()->addRowToSkip($rowNum); + continue; + } $rowScope = $this->getRowScope($rowData); $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { - $this->_rowsToSkip[$rowNum] = true; - // skip rows when SKU is NULL + $this->getErrorAggregator()->addRowToSkip($rowNum); continue; } elseif (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row @@ -1359,7 +1380,7 @@ protected function _saveProducts() } else { $rowSku = null; // sign for child rows to be skipped - $this->_rowsToSkip[$rowNum] = true; + $this->getErrorAggregator()->addRowToSkip($rowNum); continue; } } @@ -1451,25 +1472,35 @@ protected function _saveProducts() $imagePath = $allImagesFromBunch[$mediaImage]; if (isset($existingImages[$imagePath]) && in_array($rowSku, $existingImages[$imagePath])) { if (!array_key_exists($mediaImage, $uploadedGalleryFiles)) { - $uploadedGalleryFiles[$mediaImage] = $this->_uploadMediaFiles( + $uploadedFile = $this->_uploadMediaFiles( trim($mediaImage), true ); + if ($uploadedFile) { + $uploadedGalleryFiles[$mediaImage] = $uploadedFile; + } else { + $this->addRowError(ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $rowNum, null, null, ProcessingError::ERROR_LEVEL_NOT_CRITICAL); + } } } elseif (!isset($existingImages[$imagePath])) { if (!array_key_exists($mediaImage, $uploadedGalleryFiles)) { - $uploadedGalleryFiles[$mediaImage] = $this->_uploadMediaFiles( + $uploadedFile = $this->_uploadMediaFiles( trim($mediaImage), true ); - $newImagePath = $uploadedGalleryFiles[$mediaImage]; - $existingImages[$newImagePath][] = $rowSku; + if ($uploadedFile) { + $uploadedGalleryFiles[$mediaImage] = $uploadedFile; + $newImagePath = $uploadedGalleryFiles[$mediaImage]; + $existingImages[$newImagePath][] = $rowSku; + } else { + $this->addRowError(ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $rowNum, null, null, ProcessingError::ERROR_LEVEL_NOT_CRITICAL); + } } - $rowData[self::COL_MEDIA_IMAGE][] = $uploadedGalleryFiles[$mediaImage]; - if (!empty($rowData[self::COL_MEDIA_IMAGE]) && is_array($rowData[self::COL_MEDIA_IMAGE])) { - $position = array_search($mediaImage, $mediaGalleryImages); - foreach ($rowData[self::COL_MEDIA_IMAGE] as $mediaImage) { - if (!empty($mediaImage)) { + if (isset($uploadedGalleryFiles[$mediaImage])) { + $rowData[self::COL_MEDIA_IMAGE][] = $uploadedGalleryFiles[$mediaImage]; + if (!empty($rowData[self::COL_MEDIA_IMAGE]) && is_array($rowData[self::COL_MEDIA_IMAGE])) { + $position = array_search($mediaImage, $mediaGalleryImages); + foreach ($rowData[self::COL_MEDIA_IMAGE] as $mediaImage) { $mediaGallery[$rowSku][] = [ 'attribute_id' => $this->getMediaGalleryAttributeId(), 'label' => isset($mediaGalleryLabels[$position]) ? $mediaGalleryLabels[$position] : '', @@ -1539,10 +1570,7 @@ protected function _saveProducts() $product = $this->_proxyProdFactory->create(['data' => $rowData]); foreach ($rowData as $attrCode => $attrValue) { - if (!isset($this->_attributeCache[$attrCode])) { - $this->_attributeCache[$attrCode] = $resource->getAttribute($attrCode); - } - $attribute = $this->_attributeCache[$attrCode]; + $attribute = $this->retrieveAttributeByCode($attrCode); if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) { // skip attribute processing for SCOPE_NULL rows @@ -1574,16 +1602,7 @@ protected function _saveProducts() } } foreach ($storeIds as $storeId) { - if ('multiselect' == $attribute->getFrontendInput()) { - if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) { - $attributes[$attrTable][$rowSku][$attrId][$storeId] = ''; - } else { - $attributes[$attrTable][$rowSku][$attrId][$storeId] .= ','; - } - $attributes[$attrTable][$rowSku][$attrId][$storeId] .= $attrValue; - } else { - $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue; - } + $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue; } // restore 'backend_model' to avoid 'default' setting $attribute->setBackendModel($backModel); @@ -1987,6 +2006,23 @@ protected function _saveStockItem() return $this; } + /** + * Retrieve attribute by code + * + * @param string $attrCode + * @return mixed + */ + public function retrieveAttributeByCode($attrCode) + { + if (!$this->_resource) { + $this->_resource = $this->_resourceFactory->create(); + } + if (!isset($this->_attributeCache[$attrCode])) { + $this->_attributeCache[$attrCode] = $this->_resource->getAttribute($attrCode); + } + return $this->_attributeCache[$attrCode]; + } + /** * Attribute set ID-to-name pairs getter. * @@ -2067,9 +2103,6 @@ public function getCategoryProcessor() */ public function getRowScope(array $rowData) { - if (empty($rowData[self::COL_SKU])) { - return self::SCOPE_NULL; - } if (empty($rowData[self::COL_STORE])) { return self::SCOPE_DEFAULT; } @@ -2088,11 +2121,9 @@ public function getRowScope(array $rowData) */ public function validateRow(array $rowData, $rowNum) { - // SKU is remembered through all product rows - static $sku = null; if (isset($this->_validatedRows[$rowNum])) { // check that row is already validated - return !isset($this->_invalidRows[$rowNum]); + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } $this->_validatedRows[$rowNum] = true; @@ -2152,10 +2183,10 @@ public function validateRow(array $rowData, $rowNum) if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); } elseif (!isset( - $rowData[self::COL_ATTR_SET] - ) || !isset( - $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]] - ) + $rowData[self::COL_ATTR_SET] + ) || !isset( + $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]] + ) ) { $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); } elseif (is_null($this->skuProcessor->getNewSku($sku))) { @@ -2169,13 +2200,13 @@ public function validateRow(array $rowData, $rowNum) ] ); } - if (isset($this->_invalidRows[$rowNum])) { + if ($this->getErrorAggregator()->isRowInvalid($rowNum)) { // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already $sku = false; } } - if (!isset($this->_invalidRows[$rowNum])) { + if (!$this->getErrorAggregator()->isRowInvalid($rowNum)) { $newSku = $this->skuProcessor->getNewSku($sku); // set attribute set code into row data for followed attribute validation in type model $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code']; @@ -2193,7 +2224,7 @@ public function validateRow(array $rowData, $rowNum) // validate custom options $this->getOptionEntity()->validateRow($rowData, $rowNum); - return !isset($this->_invalidRows[$rowNum]); + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } /** @@ -2276,16 +2307,18 @@ protected function _saveValidatedBunches() $source = $this->_getSource(); $source->rewind(); while ($source->valid()) { - if ($this->_errorsCount >= $this->_errorsLimit) { - // errors limit check - return $this; + try { + $rowData = $source->current(); + } catch (\InvalidArgumentException $e) { + $this->addRowError($e->getMessage(), $this->_processedRowsCount); + $this->_processedRowsCount++; + $source->next(); + continue; } - $rowData = $source->current(); $rowData = $this->_customFieldsMapping($rowData); $this->validateRow($rowData, $source->key()); - $this->_processedRowsCount++; $source->next(); } $this->getOptionEntity()->validateAmbiguousData(); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index ea10b871516b1..b8a629ca16d54 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -10,6 +10,7 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\Framework\App\Resource; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Entity class which provide possibility to import product custom options @@ -305,7 +306,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * @param \Magento\ImportExport\Model\Resource\Import\Data $importData - * @param \Magento\Framework\App\Resource $resource + * @param Resource $resource * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Store\Model\StoreManagerInterface $_storeManager * @param \Magento\Catalog\Model\ProductFactory $productFactory @@ -314,7 +315,9 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param \Magento\Catalog\Helper\Data $catalogData * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param array $data + * @throws \Magento\Framework\Exception\LocalizedException * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -329,6 +332,7 @@ public function __construct( \Magento\Catalog\Helper\Data $catalogData, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Framework\Stdlib\DateTime $dateTime, + ProcessingErrorAggregatorInterface $errorAggregator, array $data = [] ) { $this->_resource = $resource; @@ -359,6 +363,8 @@ public function __construct( $this->_isPriceGlobal = $this->_catalogData->isPriceGlobal(); } + $this->errorAggregator = $errorAggregator; + $this->_initSourceEntities($data)->_initTables($data)->_initStores($data); $this->_initMessageTemplates(); @@ -376,32 +382,32 @@ protected function _initMessageTemplates() // @codingStandardsIgnoreStart $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_STORE, - __('Please enter a correct value for "store."') + __('Value for \'price\' sub attribute in \'store\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_TYPE, - __('Please enter a correct value for "type."') + __('Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\'') ); $this->_productEntity->addMessageTemplate(self::ERROR_EMPTY_TITLE, __('Please enter a value for title.')); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_PRICE, - __('Please enter a correct value for "price."') + __('Value for \'price\' sub attribute in \'custom_options\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_MAX_CHARACTERS, - __('Please enter a correct value for "maximum characters."') + __('Value for \'maximum characters\' sub attribute in \'custom_options\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_SORT_ORDER, - __('Please enter a correct value for "sort order."') + __('Value for \'sort order\' sub attribute in \'custom_options\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_ROW_PRICE, - __('Please enter a correct value for "value price."') + __('Value for \'value price\' sub attribute in \'custom_options\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_ROW_SORT, - __('Please enter a correct value for "sort order."') + __('Value for \'sort order\' sub attribute in \'custom_options\' attribute contains incorrect value') ); $this->_productEntity->addMessageTemplate( self::ERROR_AMBIGUOUS_NEW_NAMES, diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php index 4e986422eee52..c639fcc4acf25 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php @@ -61,6 +61,26 @@ interface RowValidatorInterface extends \Magento\Framework\Validator\ValidatorIn const ERROR_INVALID_WEIGHT = 'invalidWeight'; + const ERROR_EXCEEDED_MAX_LENGTH = 'exceededMaxLength'; + + const ERROR_INVALID_ATTRIBUTE_TYPE = 'invalidAttributeType'; + + const ERROR_INVALID_ATTRIBUTE_DECIMAL = 'invalidAttributeDecimal'; + + const ERROR_ABSENT_REQUIRED_ATTRIBUTE = 'absentRequiredAttribute'; + + const ERROR_INVALID_ATTRIBUTE_OPTION = 'absentAttributeOption'; + + const ERROR_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicatedUniqueAttribute'; + + const ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS = 'invalidVariationsCustomOptions'; + + const ERROR_INVALID_MEDIA_URL_OR_PATH = 'invalidMediaUrlPath'; + + const ERROR_MEDIA_URL_NOT_ACCESSIBLE = 'mediaUrlNotAvailable'; + + const ERROR_MEDIA_PATH_NOT_ACCESSIBLE = 'mediaPathNotAvailable'; + /** * Value that means all entities (e.g. websites, groups etc.) */ @@ -69,7 +89,8 @@ interface RowValidatorInterface extends \Magento\Framework\Validator\ValidatorIn /** * Initialize validator * + * @param \Magento\CatalogImportExport\Model\Import\Product $context * @return $this */ - public function init(); + public function init($context); } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 79b0cfb55e484..7fe262fbf57c0 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -6,11 +6,13 @@ namespace Magento\CatalogImportExport\Model\Import\Product\Type; use Magento\Framework\App\Resource; +use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product; /** * Import entity abstract product type model * - * @author Magento Core Team + * @SuppressWarnings(PHPMD.TooManyFields) */ abstract class AbstractType { @@ -21,6 +23,13 @@ abstract class AbstractType */ public static $commonAttributesCache = []; + /** + * Attribute Code to Id cache + * + * @var array + */ + public static $attributeCodeToId = []; + /** * Product type attribute sets and attributes parameters. * @@ -53,12 +62,22 @@ abstract class AbstractType protected $_indexValueAttributes = []; /** - * Validation failure message template definitions + * Validation failure entity specific message template definitions * * @var array */ protected $_messageTemplates = []; + /** + * Validation failure general message template definitions + * + * @var array + */ + protected $_genericMessageTemplates = [ + RowValidatorInterface::ERROR_INVALID_WEIGHT => 'Weight value is incorrect', + RowValidatorInterface::ERROR_INVALID_WEBSITE => 'Provided Website code doesn\'t exist' + ]; + /** * Column names that holds values with particular meaning. * @@ -107,7 +126,6 @@ abstract class AbstractType */ protected $connection; - /** * @param \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory $attrSetColFac * @param \Magento\Catalog\Model\Resource\Product\Attribute\CollectionFactory $prodAttrColFac @@ -136,13 +154,27 @@ public function __construct( $this->_entityModel = $params[0]; $this->_type = $params[1]; - foreach ($this->_messageTemplates as $errorCode => $message) { - $this->_entityModel->addMessageTemplate($errorCode, $message); - } + $this->initMessageTemplates( + array_merge($this->_genericMessageTemplates, $this->_messageTemplates) + ); + $this->_initAttributes(); } } + /** + * @param array $templateCollection + * @return $this + */ + protected function initMessageTemplates(array $templateCollection) + { + foreach ($templateCollection as $errorCode => $message) { + $this->_entityModel->addMessageTemplate($errorCode, $message); + } + + return $this; + } + /** * Add attribute parameters to appropriate attribute set. * @@ -160,6 +192,21 @@ protected function _addAttributeParams($attrSetName, array $attrParams, $attribu return $this; } + /** + * Retrieve product Attribute + * + * @param string $attributeCode + * @param string $attributeSet + * @return array + */ + public function retrieveAttribute($attributeCode, $attributeSet) + { + if (isset($this->_attributes[$attributeSet]) && isset($this->_attributes[$attributeSet][$attributeCode])) { + return $this->_attributes[$attributeSet][$attributeCode]; + } + return []; + } + /** * Return product attributes for its attribute set specified in row data. * @@ -197,29 +244,23 @@ protected function _initAttributes() ) ); $absentKeys = []; - foreach ($entityAttributes as $item) { - $attributeId = $item['attribute_id']; - $attributeSetName = $item['attribute_set_name']; - - if (!isset(self::$commonAttributesCache[$attributeId])) { - if (!isset($absentKeys[$attributeSetName])) { - $absentKeys[$attributeSetName] = []; + foreach ($entityAttributes as $attributeRow) { + if (!isset(self::$commonAttributesCache[$attributeRow['attribute_id']])) { + if (!isset($absentKeys[$attributeRow['attribute_set_name']])) { + $absentKeys[$attributeRow['attribute_set_name']] = []; } - $absentKeys[$attributeSetName][] = $attributeId; + $absentKeys[$attributeRow['attribute_set_name']][] = $attributeRow['attribute_id']; } } foreach ($absentKeys as $attributeSetName => $attributeIds) { $this->attachAttributesById($attributeSetName, $attributeIds); } - foreach ($entityAttributes as $item) { - $attributeId = $item['attribute_id']; - $attributeSetName = $item['attribute_set_name']; - - if (isset(self::$commonAttributesCache[$attributeId])) { - $attribute = self::$commonAttributesCache[$attributeId]; + foreach ($entityAttributes as $attributeRow) { + if (isset(self::$commonAttributesCache[$attributeRow['attribute_id']])) { + $attribute = self::$commonAttributesCache[$attributeRow['attribute_id']]; $this->_addAttributeParams( - $attributeSetName, - self::$commonAttributesCache[$attributeId], + $attributeRow['attribute_set_name'], + self::$commonAttributesCache[$attributeRow['attribute_id']], $attribute ); } @@ -262,6 +303,7 @@ protected function attachAttributesById($attributeSetName, $attributeIds) $this->_indexValueAttributes ), ]; + self::$attributeCodeToId[$attributeCode] = $attributeId; $this->_addAttributeParams( $attributeSetName, self::$commonAttributesCache[$attributeId], @@ -271,6 +313,22 @@ protected function attachAttributesById($attributeSetName, $attributeIds) } } + /** + * Retrieve attribute from cache + * + * @param string $attributeCode + * @return mixed + */ + public function retrieveAttributeFromCache($attributeCode) + { + if (isset(self::$attributeCodeToId[$attributeCode]) && $id = self::$attributeCodeToId[$attributeCode]) { + if (isset(self::$commonAttributesCache[$id])) { + return self::$commonAttributesCache[$id]; + } + } + return []; + } + /** * In case we've dynamically added new attribute option during import we need to add it to our cache * in order to keep it up to date. @@ -360,8 +418,10 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) { $error = false; $rowScope = $this->_entityModel->getRowScope($rowData); + if ((\Magento\CatalogImportExport\Model\Import\Product::SCOPE_NULL != $rowScope) && + !empty($rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_SKU])) { + - if (\Magento\CatalogImportExport\Model\Import\Product::SCOPE_NULL != $rowScope) { foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) { // check value for non-empty in the case of required attribute? if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { @@ -420,10 +480,16 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) { if (!$attrParams['is_static']) { if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { - $resultAttrs[$attrCode] = 'select' == $attrParams['type'] || - 'multiselect' == $attrParams['type'] ? $attrParams['options'][strtolower( - $rowData[$attrCode] - )] : $rowData[$attrCode]; + $resultAttrs[$attrCode] = 'select' == $attrParams['type'] ? $attrParams['options'][strtolower( + $rowData[$attrCode] + )] : $rowData[$attrCode]; + if ('multiselect' == $attrParams['type']) { + $resultAttrs[$attrCode] = []; + foreach (explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]) as $value) { + $resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)]; + } + $resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]); + } } elseif (array_key_exists($attrCode, $rowData)) { $resultAttrs[$attrCode] = $rowData[$attrCode]; } elseif ($withDefaultValue && null !== $attrParams['default_value']) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/Virtual.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/Virtual.php new file mode 100644 index 0000000000000..aff410936e08d --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/Virtual.php @@ -0,0 +1,48 @@ + + */ +class Virtual extends \Magento\CatalogImportExport\Model\Import\Product\Type\Simple +{ + /** + * Type virtual product + */ + const TYPE_VIRTUAL_PRODUCT = 'virtual'; + + /** + * Prepare attributes with default value for save. + * + * @param array $rowData + * @param bool $withDefaultValue + * @return array + */ + public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDefaultValue = true) + { + $resultAttrs = parent::prepareAttributesWithDefaultValueForSave($rowData, $withDefaultValue); + $resultAttrs = array_merge($resultAttrs, $this->setWeightVirtualProduct($rowData)); + return $resultAttrs; + } + + /** + * Set weight is null if product is virtual + * + * @param array $rowData + * @return array + */ + protected function setWeightVirtualProduct(array $rowData) + { + $result = []; + if ($rowData['product_type'] == self::TYPE_VIRTUAL_PRODUCT) { + $result['weight'] = null; + } + return $result; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 0f44d54947164..980535ce2d8b5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product; +use \Magento\CatalogImportExport\Model\Import\Product; use \Magento\Framework\Validator\AbstractValidator; class Validator extends AbstractValidator implements RowValidatorInterface @@ -15,20 +16,207 @@ class Validator extends AbstractValidator implements RowValidatorInterface protected $validators = []; /** + * @var \Magento\CatalogImportExport\Model\Import\Product + */ + protected $context; + + /** + * @var \Magento\Framework\Stdlib\StringUtils + */ + protected $string; + + /** + * @var array + */ + protected $_uniqueAttributes; + + /** + * @var array + */ + protected $_rowData; + + /** + * @param \Magento\Framework\Stdlib\StringUtils $string * @param RowValidatorInterface[] $validators */ - public function __construct($validators = []) - { + public function __construct( + \Magento\Framework\Stdlib\StringUtils $string, + $validators = [] + ) { + $this->string = $string; $this->validators = $validators; } + /** + * @param mixed $attrCode + * @param string $type + * @return bool + */ + protected function textValidation($attrCode, $type) + { + $val = $this->string->cleanString($this->_rowData[$attrCode]); + if ($type == 'text') { + $valid = $this->string->strlen($val) < Product::DB_MAX_TEXT_LENGTH; + } else { + $valid = $this->string->strlen($val) < Product::DB_MAX_VARCHAR_LENGTH; + } + if (!$valid) { + $this->_addMessages([RowValidatorInterface::ERROR_EXCEEDED_MAX_LENGTH]); + } + return $valid; + } + + /** + * @param mixed $attrCode + * @param string $type + * @return bool + */ + protected function numericValidation($attrCode, $type) + { + $val = trim($this->_rowData[$attrCode]); + if ($type == 'int') { + $valid = (string)(int)$val === $val; + } else { + $valid = is_numeric($val); + } + if (!$valid) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate(RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE), + $attrCode, + $type + ) + ] + ); + } + return $valid; + } + + /** + * @param string $attrCode + * @param array $attrParams + * @param array $rowData + * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function isAttributeValid($attrCode, array $attrParams, array $rowData) + { + $this->_rowData = $rowData; + if (!empty($attrParams['apply_to']) && !in_array($rowData['product_type'], $attrParams['apply_to'])) { + return true; + } + if ($attrCode == Product::COL_SKU || $attrParams['is_required'] + && ($this->context->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE + || ($this->context->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND + && !isset($this->context->getOldSku()[$rowData[$attrCode]]))) + ) { + if (!isset($rowData[$attrCode]) || !strlen(trim($rowData[$attrCode]))) { + $valid = false; + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate( + RowValidatorInterface::ERROR_VALUE_IS_REQUIRED + ), + $attrCode + ) + ] + ); + return $valid; + } + } + + if (!strlen(trim($rowData[$attrCode]))) { + return true; + } + switch ($attrParams['type']) { + case 'varchar': + case 'text': + $valid = $this->textValidation($attrCode, $attrParams['type']); + break; + case 'decimal': + case 'int': + $valid = $this->numericValidation($attrCode, $attrParams['type']); + break; + case 'select': + case 'multiselect': + $values = explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]); + $valid = true; + foreach ($values as $value) { + $valid = $valid || isset($attrParams['options'][strtolower($value)]); + } + if (!$valid) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate( + RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION + ), + $attrCode + ) + ] + ); + } + break; + case 'datetime': + $val = trim($rowData[$attrCode]); + $valid = strtotime($val) !== false; + if (!$valid) { + $this->_addMessages([RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE]); + } + break; + default: + $valid = true; + break; + } + + if ($valid && !empty($attrParams['is_unique'])) { + if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]]) + && ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])) { + $this->_addMessages([RowValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE]); + return false; + } + $this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] = $rowData[Product::COL_SKU]; + } + return (bool)$valid; + + } + + /** + * @return bool + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function isValidAttributes() + { + $this->_clearMessages(); + if (!isset($this->_rowData['product_type'])) { + return false; + } + $entityTypeModel = $this->context->retrieveProductTypeByName($this->_rowData['product_type']); + if ($entityTypeModel) { + foreach ($this->_rowData as $attrCode => $attrValue) { + $attrParams = $entityTypeModel->retrieveAttributeFromCache($attrCode); + if ($attrParams) { + $this->isAttributeValid($attrCode, $attrParams, $this->_rowData); + } + } + if ($this->getMessages()) { + return false; + } + } + return true; + } + /** * {@inheritdoc} */ public function isValid($value) { - $returnValue = true; + $this->_rowData = $value; $this->_clearMessages(); + $returnValue = $this->isValidAttributes(); foreach ($this->validators as $validator) { if (!$validator->isValid($value)) { $returnValue = false; @@ -39,12 +227,14 @@ public function isValid($value) } /** - * {@inheritdoc} + * @param \Magento\CatalogImportExport\Model\Import\Product $context + * @return $this */ - public function init() + public function init($context) { + $this->context = $context; foreach ($this->validators as $validator) { - $validator->init(); + $validator->init($context); } } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractImportValidator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractImportValidator.php new file mode 100644 index 0000000000000..519a5457bcd74 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractImportValidator.php @@ -0,0 +1,27 @@ +context = $context; + return $this; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractPrice.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractPrice.php index 1f6f41ffaa5b4..8810272f16605 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractPrice.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/AbstractPrice.php @@ -5,10 +5,10 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product\Validator; -use Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; -abstract class AbstractPrice extends AbstractValidator implements RowValidatorInterface +abstract class AbstractPrice extends AbstractImportValidator implements RowValidatorInterface { /** * @var \Magento\Customer\Api\GroupRepositoryInterface @@ -42,11 +42,11 @@ public function __construct( /** * {@inheritdoc} */ - public function init() + public function init($context) { foreach ($this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems() as $group) { $this->customerGroups[$group->getId()] = true; } - return $this; + return parent::init($context); } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/GroupPrice.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/GroupPrice.php index 2d27b9867a9a4..56d6aebd0441b 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/GroupPrice.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/GroupPrice.php @@ -31,9 +31,9 @@ public function __construct( /** * {@inheritdoc} */ - public function init() + public function init($context) { - return parent::init(); + return parent::init($context); } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php index fd3b69e0ac5a7..c5c96ea23462f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php @@ -5,25 +5,99 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product\Validator; -use \Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; -class Media extends AbstractValidator implements RowValidatorInterface +class Media extends AbstractImportValidator implements RowValidatorInterface { + const URL_REGEXP = '|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i'; + + const PATH_REGEXP = '#^(?!.*[\\/]\.{2}[\\/])(?!\.{2}[\\/])[-\w.\\/]+$#'; + + const ADDITIONAL_IMAGES = 'additional_images'; + + const ADDITIONAL_IMAGES_DELIMITER = ','; + + /** @var array */ + protected $mediaAttributes = ['image', 'small_image', 'thumbnail']; + /** * {@inheritdoc} */ - public function init() + public function init($context) { - return $this; + return parent::init($context); } /** - * {@inheritdoc} + * @param string $string + * @return bool + */ + protected function checkValidUrl($string) + { + return preg_match(self::URL_REGEXP, $string); + } + + /** + * @param string $string + * @return bool + */ + protected function checkPath($string) + { + return preg_match(self::PATH_REGEXP, $string); + } + + /** + * @param string $path + * @return bool + */ + protected function checkFileExists($path) + { + return file_exists($path); + } + + /** + * Validate value + * + * @param array $value + * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function isValid($value) { $this->_clearMessages(); - return true; + $valid = true; + foreach ($this->mediaAttributes as $attribute) { + if (isset($value[$attribute]) && strlen($value[$attribute])) { + if (!$this->checkPath($value[$attribute]) && !$this->checkValidUrl($value[$attribute])) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate(self::ERROR_INVALID_MEDIA_URL_OR_PATH), + $attribute + ) + ] + ); + $valid = false; + } + } + } + if (isset($value[self::ADDITIONAL_IMAGES]) && strlen($value[self::ADDITIONAL_IMAGES])) { + foreach (explode(self::ADDITIONAL_IMAGES_DELIMITER, $value[self::ADDITIONAL_IMAGES]) as $image) { + if (!$this->checkPath($image) && !$this->checkValidUrl($image)) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate(self::ERROR_INVALID_MEDIA_URL_OR_PATH), + self::ADDITIONAL_IMAGES + ) + ] + ); + $valid = false; + } + break; + } + } + return $valid; } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Quantity.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Quantity.php new file mode 100644 index 0000000000000..4e40cc5cc47c9 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Quantity.php @@ -0,0 +1,41 @@ +_clearMessages(); + if (!empty($value['qty']) && (!is_numeric($value['qty']) || $value['qty'] < 0)) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate(self::ERROR_INVALID_ATTRIBUTE_TYPE), + 'qty', + 'decimal' + ) + ] + ); + return false; + } + return true; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php index ac3329f0d8f04..44c4bbbb7a0df 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php @@ -5,10 +5,10 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product\Validator; -use \Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; -class SuperProductsSku extends AbstractValidator implements RowValidatorInterface +class SuperProductsSku extends AbstractImportValidator implements RowValidatorInterface { /** * @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor @@ -27,9 +27,9 @@ public function __construct( /** * {@inheritdoc} */ - public function init() + public function init($context) { - return $this; + return parent::init($context); } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/TierPrice.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/TierPrice.php index 489bbe11a3f9f..aeb9862a7d94a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/TierPrice.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/TierPrice.php @@ -31,9 +31,9 @@ public function __construct( /** * {@inheritdoc} */ - public function init() + public function init($context) { - return parent::init(); + return parent::init($context); } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php index 0547d7820036f..3865bc73e5a65 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php @@ -5,10 +5,11 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product\Validator; -use \Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; -class Website extends AbstractValidator implements RowValidatorInterface +class Website extends AbstractImportValidator implements RowValidatorInterface { /** * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver @@ -26,9 +27,9 @@ public function __construct(\Magento\CatalogImportExport\Model\Import\Product\St /** * {@inheritdoc} */ - public function init() + public function init($context) { - return $this; + return parent::init($context); } /** @@ -37,11 +38,16 @@ public function init() public function isValid($value) { $this->_clearMessages(); - if (!empty($value['_product_websites']) - && !$this->storeResolver->getWebsiteCodeToId($value['_product_websites']) - ) { - $this->_addMessages([self::ERROR_INVALID_WEBSITE]); - return false; + if (empty($value[ImportProduct::COL_PRODUCT_WEBSITES])) { + return true; + } + $separator = $this->context->getMultipleValueSeparator(); + $websites = explode($separator, $value[ImportProduct::COL_PRODUCT_WEBSITES]); + foreach ($websites as $website) { + if (!$this->storeResolver->getWebsiteCodeToId($website)) { + $this->_addMessages([self::ERROR_INVALID_WEBSITE]); + return false; + } } return true; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Weight.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Weight.php index 5e6cea959fa8e..8f69496d32956 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Weight.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Weight.php @@ -5,17 +5,17 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product\Validator; -use \Magento\Framework\Validator\AbstractValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; -class Weight extends AbstractValidator implements RowValidatorInterface +class Weight extends AbstractImportValidator implements RowValidatorInterface { /** * {@inheritdoc} */ - public function init() + public function init($context) { - return $this; + return parent::init($context); } /** @@ -25,7 +25,15 @@ public function isValid($value) { $this->_clearMessages(); if (!empty($value['weight']) && (!is_numeric($value['weight']) || $value['weight'] < 0)) { - $this->_addMessages([self::ERROR_INVALID_WEIGHT]); + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate(self::ERROR_INVALID_ATTRIBUTE_TYPE), + 'weight', + 'decimal' + ) + ] + ); return false; } return true; diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index 7ce58764622e7..4b07ee981af02 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -118,9 +118,10 @@ protected function setUp() false ); - $entityAttributes = [ - 'attribute_id' => 'attributeSetName' - ]; + $entityAttributes = [[ + 'attribute_id' => 'attribute_id', + 'attribute_set_name' => 'attributeSetName', + ]]; $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturn(['option1', 'option2']); @@ -147,9 +148,7 @@ protected function setUp() ->method('addFieldToFilter') ->with( 'main_table.attribute_id', - ['in' => [ - key($entityAttributes) - ]] + ['in' => ['attribute_id']] ) ->willReturn([$attribute]); @@ -193,14 +192,7 @@ protected function setUp() $this->connection ->expects($this->any()) ->method('fetchAll') - ->will($this->returnValue( - [ - [ - 'attribute_id' => 'attribute_id', - 'attribute_set_name' => 'attribute_set_name' - ] - ] - )); + ->will($this->returnValue($entityAttributes)); $this->resource = $this->getMock( '\Magento\Framework\App\Resource', @@ -286,6 +278,7 @@ public function testIsRowValidError() { $rowData = [ '_attribute_set' => 'attribute_set_name', + 'sku' => 'sku' ]; $rowNum = 1; $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(1); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index cb9323439e3c2..da52456ccaca7 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -11,18 +11,13 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class OptionTest extends \PHPUnit_Framework_TestCase +class OptionTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * Path to csv file to import */ const PATH_TO_CSV_FILE = '/_files/product_with_custom_options.csv'; - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $_helper; - /** * Test store parametes * @@ -208,12 +203,18 @@ class OptionTest extends \PHPUnit_Framework_TestCase */ protected $_iteratorPageSize = 100; + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface + */ + protected $errorAggregator; + /** * Init entity adapter model */ protected function setUp() { - $this->_helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + parent::setUp(); + $addExpectations = false; $deleteBehavior = false; $testName = $this->getName(true); @@ -231,7 +232,6 @@ protected function setUp() $scopeConfig = $this->getMock('Magento\Framework\App\Config\ScopeConfigInterface'); - $modelClassName = '\Magento\CatalogImportExport\Model\Import\Product\Option'; $modelClassArgs = [ $this->getMock('Magento\ImportExport\Model\Resource\Import\Data', [], [], '', false), $this->getMock('Magento\Framework\App\Resource', [], [], '', false), @@ -255,9 +255,17 @@ protected function setUp() $catalogDataMock, $scopeConfig, new \Magento\Framework\Stdlib\DateTime(), + $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface', + [], + [], + '', + false + ), $this->_getModelDependencies($addExpectations, $deleteBehavior, $doubleOptions) ]; + $modelClassName = '\Magento\CatalogImportExport\Model\Import\Product\Option'; $class = new \ReflectionClass($modelClassName); $this->_model = $class->newInstanceArgs($modelClassArgs); // Create model mock with rewritten _getMultiRowFormat method to support test data with the old format. @@ -381,11 +389,12 @@ protected function _getSourceDataMocks($addExpectations, $doubleOptions) $this->_productEntity = $this->getMock( 'Magento\CatalogImportExport\Model\Import\Product', - null, + ['getErrorAggregator'], [], '', false ); + $this->_productEntity->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $productModelMock = $this->getMock('stdClass', ['getProductEntitiesInfo']); $productModelMock->expects( @@ -694,13 +703,13 @@ public function testValidateRowNoCustomOption() public function testValidateRow(array $rowData, array $errors) { $this->_bypassModelMethodGetMultiRowFormat($rowData); - if (empty($errors)) { $this->assertTrue($this->_modelMock->validateRow($rowData, 0)); } else { $this->assertFalse($this->_modelMock->validateRow($rowData, 0)); } - $this->assertAttributeEquals($errors, '_errors', $this->_productEntity); + $resultErrors = $this->_productEntity->getErrorAggregator()->getRowsGroupedByErrorCode([], [], false); + $this->assertEquals($errors, $resultErrors); } /** @@ -741,7 +750,8 @@ public function testValidateAmbiguousData( } else { $this->assertFalse($this->_modelMock->validateAmbiguousData()); } - $this->assertAttributeEquals($errors, '_errors', $this->_productEntity); + $resultErrors = $this->_productEntity->getErrorAggregator()->getRowsGroupedByErrorCode([], [], false); + $this->assertEquals($errors, $resultErrors); } /** @@ -760,73 +770,55 @@ public function validateRowDataProvider() 'main_invalid_store' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_invalid_store.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_STORE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_STORE => [1] ] ], 'main_incorrect_type' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_incorrect_type.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_TYPE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_TYPE => [1] ] ], 'main_no_title' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_no_title.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_EMPTY_TITLE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_EMPTY_TITLE => [1] ] ], 'main_empty_title' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_empty_title.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_EMPTY_TITLE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_EMPTY_TITLE => [1] ] ], 'main_invalid_price' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_invalid_price.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_PRICE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_PRICE => [1] ] ], 'main_invalid_max_characters' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_invalid_max_characters.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_MAX_CHARACTERS => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_MAX_CHARACTERS => [1] ] ], 'main_max_characters_less_zero' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_max_characters_less_zero.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_MAX_CHARACTERS => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_MAX_CHARACTERS => [1] ] ], 'main_invalid_sort_order' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_invalid_sort_order.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_SORT_ORDER => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_SORT_ORDER => [1] ] ], 'main_sort_order_less_zero' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_sort_order_less_zero.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_SORT_ORDER => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_SORT_ORDER => [1] ] ], 'secondary_valid' => [ @@ -836,33 +828,25 @@ public function validateRowDataProvider() 'secondary_invalid_store' => [ '$rowData' => include __DIR__ . '/_files/row_data_secondary_invalid_store.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_STORE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_STORE => [1] ] ], 'secondary_incorrect_price' => [ '$rowData' => include __DIR__ . '/_files/row_data_secondary_incorrect_price.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_PRICE => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_PRICE => [1] ] ], 'secondary_incorrect_row_sort' => [ '$rowData' => include __DIR__ . '/_files/row_data_secondary_incorrect_row_sort.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_SORT => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_SORT => [1] ] ], 'secondary_row_sort_less_zero' => [ '$rowData' => include __DIR__ . '/_files/row_data_secondary_row_sort_less_zero.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_SORT => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_INVALID_ROW_SORT => [1] ] ] ]; @@ -879,10 +863,7 @@ public function validateAmbiguousDataDataProvider() 'ambiguity_several_input_rows' => [ '$rowData' => include __DIR__ . '/_files/row_data_main_valid.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_NEW_NAMES => [ - [1, null], - [2, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_NEW_NAMES => [2, 2] ], '$behavior' => null, '$numberOfValidations' => 2 @@ -890,18 +871,14 @@ public function validateAmbiguousDataDataProvider() 'ambiguity_different_type' => [ '$rowData' => include __DIR__ . '/_files/row_data_ambiguity_different_type.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_TYPES => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_TYPES => [1] ], '$behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND ], 'ambiguity_several_db_rows' => [ '$rowData' => include __DIR__ . '/_files/row_data_ambiguity_several_db_rows.php', '$errors' => [ - \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_OLD_NAMES => [ - [1, null] - ] + \Magento\CatalogImportExport\Model\Import\Product\Option::ERROR_AMBIGUOUS_OLD_NAMES => [1] ], '$behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND ] @@ -933,15 +910,15 @@ public function testParseRequiredData() false ); - $model = $this->_helper->getObject( + $model = $this->objectManagerHelper->getObject( 'Magento\CatalogImportExport\Model\Import\Product\Option', [ 'data' => [ 'data_source_model' => $modelData, 'product_model' => $productModel, - 'option_collection' => $this->_helper->getObject('stdClass'), + 'option_collection' => $this->objectManagerHelper->getObject('stdClass'), 'product_entity' => $productEntity, - 'collection_by_pages_iterator' => $this->_helper->getObject('stdClass'), + 'collection_by_pages_iterator' => $this->objectManagerHelper->getObject('stdClass'), 'page_size' => 5000, 'stores' => [] ] diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/VirtualTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/VirtualTest.php new file mode 100644 index 0000000000000..4526c05df7dae --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/VirtualTest.php @@ -0,0 +1,96 @@ +getMock( + 'Magento\CatalogImportExport\Model\Import\Product\Type\Virtual', + null, + [], + '', + false + ); + + $this->setPropertyValue( + $virtualModelMock, + '_attributes', + [ + 'Default' => [ + 'name' => [ + 'id' => '69', + 'code' => 'name', + 'is_global' => '0', + 'is_required' => '1', + 'is_unique' => '0', + 'frontend_label' => 'Name', + 'is_static' => false, + 'apply_to' => + [ + ], + 'type' => 'varchar', + 'default_value' => null, + 'options' => + [ + ], + ], + 'sku' => [ + 'id' => '70', + 'code' => 'sku', + 'is_global' => '1', + 'is_required' => '1', + 'is_unique' => '1', + 'frontend_label' => 'SKU', + 'is_static' => true, + 'apply_to' => + [ + ], + 'type' => 'varchar', + 'default_value' => null, + 'options' => + [ + ], + ] + ] + ] + ); + + $rowData = [ + '_attribute_set' => 'Default', + 'sku' => 'downloadablesku1', + 'product_type' => 'virtual', + 'name' => 'Downloadable Product 1' + ]; + + $expectedResult = [ + 'name' => 'Downloadable Product 1', + 'weight' => null + ]; + + $result = $virtualModelMock->prepareAttributesWithDefaultValueForSave($rowData); + $this->assertEquals($result, $expectedResult); + } + + /** + * @param $object + * @param $property + * @param $value + */ + protected function setPropertyValue(&$object, $property, $value) + { + $reflection = new \ReflectionClass(get_class($object)); + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + return $object; + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/MediaTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/MediaTest.php index cb0a32acff56e..fc7259582171c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/MediaTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/MediaTest.php @@ -30,7 +30,7 @@ protected function setUp() public function testInit() { - $result = $this->media->init(); + $result = $this->media->init(null); $this->assertEquals($this->media, $result); } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php index 9b82048555d41..94dcf6ee2cfba 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php @@ -63,7 +63,7 @@ protected function processInit($groupId) $group = $this->getMock('Magento\Customer\Model\Data\Group', [], [], '', false); $group->expects($this->once())->method('getId')->willReturn($groupId); $searchResult->expects($this->once())->method('getItems')->willReturn([$group]); - return $this->tierPrice->init(); + return $this->tierPrice->init(null); } public function testInit() diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php index d0db94e92962e..8e6df5186c075 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php @@ -19,6 +19,9 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase /** @var array */ protected $validators = []; + /** @var \Magento\CatalogImportExport\Model\Import\Product|\PHPUnit_Framework_MockObject_MockObject */ + protected $context; + /** @var Validator\Media|\PHPUnit_Framework_MockObject_MockObject */ protected $validatorOne; @@ -27,6 +30,24 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $entityTypeModel = $this->getMock( + 'Magento\CatalogImportExport\Model\Import\Product\Type\Simple', + ['retrieveAttributeFromCache'], + [], + '', + false + ); + $entityTypeModel->expects($this->any())->method('retrieveAttributeFromCache')->willReturn([]); + $this->context = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Product', + ['retrieveProductTypeByName', 'retrieveMessageTemplate'], + [], + '', + false + ); + $this->context->expects($this->any())->method('retrieveProductTypeByName')->willReturn($entityTypeModel); + $this->context->expects($this->any())->method('retrieveMessageTemplate')->willReturn(''); + $this->validatorOne = $this->getMock( 'Magento\CatalogImportExport\Model\Import\Product\Validator\Media', [], @@ -48,11 +69,12 @@ protected function setUp() 'Magento\CatalogImportExport\Model\Import\Product\Validator', ['validators' => $this->validators] ); + $this->validator->init($this->context); } public function testIsValidCorrect() { - $value = 'val'; + $value = ['product_type' => 'simple']; $this->validatorOne->expects($this->once())->method('isValid')->with($value)->willReturn(true); $this->validatorTwo->expects($this->once())->method('isValid')->with($value)->willReturn(true); $result = $this->validator->isValid($value); @@ -61,7 +83,7 @@ public function testIsValidCorrect() public function testIsValidIncorrect() { - $value = 'val'; + $value = ['product_type' => 'simple']; $this->validatorOne->expects($this->once())->method('isValid')->with($value)->willReturn(true); $this->validatorTwo->expects($this->once())->method('isValid')->with($value)->willReturn(false); $messages = ['errorMessage']; @@ -75,6 +97,6 @@ public function testInit() { $this->validatorOne->expects($this->once())->method('init'); $this->validatorTwo->expects($this->once())->method('init'); - $this->validator->init(); + $this->validator->init(null); } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 7161180b355b0..8f500954457d5 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -8,7 +8,6 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Stdlib\DateTime; -use Zend\Server\Reflection\ReflectionClass; /** * Class ProductTest @@ -17,7 +16,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ProductTest extends \PHPUnit_Framework_TestCase +class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { const MEDIA_DIRECTORY = 'media/import'; @@ -151,11 +150,18 @@ class ProductTest extends \PHPUnit_Framework_TestCase /** @var \Magento\CatalogImportExport\Model\Import\Product */ protected $importProduct; + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface + */ + protected $errorAggregator; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { + parent::setUp(); + /* For parent object construct */ $this->jsonHelper = $this->getMockBuilder('\Magento\Framework\Json\Helper\Data') @@ -294,6 +300,7 @@ protected function setUp() ->getMock(); $this->validator = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Validator') + ->setMethods(['isAttributeValid', 'getMessages', 'isValid']) ->disableOriginalConstructor() ->getMock(); $this->objectRelationProcessor = @@ -309,6 +316,8 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->errorAggregator = $this->getErrorAggregatorObject(); + $this->data = []; $this->_objectConstructor() @@ -325,6 +334,7 @@ protected function setUp() $this->resource, $this->resourceHelper, $this->string, + $this->errorAggregator, $this->_eventManager, $this->stockRegistry, $this->stockConfiguration, @@ -368,15 +378,15 @@ protected function _objectConstructor() false ); $this->optionEntity = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Option') - ->disableOriginalConstructor()->getMock(); + ->disableOriginalConstructor()->getMock(); $this->optionFactory->expects($this->once())->method('create')->willReturn($this->optionEntity); $this->_filesystem->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::ROOT) - ->will($this->returnValue(self::MEDIA_DIRECTORY)); + ->method('getDirectoryWrite') + ->with(DirectoryList::ROOT) + ->will($this->returnValue(self::MEDIA_DIRECTORY)); - $this->validator->expects($this->once())->method('init'); + $this->validator->expects($this->any())->method('init'); return $this; } @@ -400,34 +410,34 @@ protected function _parentObjectConstructor() protected function _initAttributeSets() { $attributeSetOne = $this->getMockBuilder('\Magento\Eav\Model\Entity\Attribute\Set') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $attributeSetOne->expects($this->any()) - ->method('getAttributeSetName') - ->willReturn('attributeSet1'); + ->method('getAttributeSetName') + ->willReturn('attributeSet1'); $attributeSetOne->expects($this->any()) - ->method('getId') - ->willReturn('1'); + ->method('getId') + ->willReturn('1'); $attributeSetTwo = $this->getMockBuilder('\Magento\Eav\Model\Entity\Attribute\Set') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $attributeSetTwo->expects($this->any()) - ->method('getAttributeSetName') - ->willReturn('attributeSet2'); + ->method('getAttributeSetName') + ->willReturn('attributeSet2'); $attributeSetTwo->expects($this->any()) - ->method('getId') - ->willReturn('2'); + ->method('getId') + ->willReturn('2'); $attributeSetCol = [$attributeSetOne, $attributeSetTwo]; $collection = $this->getMockBuilder('\Magento\Eav\Model\Resource\Entity\Attribute\Set\Collection') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $collection->expects($this->once()) - ->method('setEntityTypeFilter') - ->with(self::ENTITY_TYPE_ID) - ->willReturn($attributeSetCol); + ->method('setEntityTypeFilter') + ->with(self::ENTITY_TYPE_ID) + ->willReturn($attributeSetCol); $this->_setColFactory->expects($this->once()) - ->method('create') - ->willReturn($collection); + ->method('create') + ->willReturn($collection); return $this; } @@ -445,18 +455,18 @@ protected function _initTypeModels() $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType') ->disableOriginalConstructor()->getMock(); $productTypeInstance->expects($this->once()) - ->method('isSuitable') - ->willReturn(true); + ->method('isSuitable') + ->willReturn(true); $productTypeInstance->expects($this->once()) - ->method('getParticularAttributes') - ->willReturn([]); + ->method('getParticularAttributes') + ->willReturn([]); $productTypeInstance->expects($this->once()) - ->method('getCustomFieldsMapping') - ->willReturn([]); + ->method('getCustomFieldsMapping') + ->willReturn([]); $this->_importConfig->expects($this->once()) - ->method('getEntityTypes') - ->with(self::ENTITY_TYPE_CODE) - ->willReturn($entityTypes); + ->method('getEntityTypes') + ->with(self::ENTITY_TYPE_CODE) + ->willReturn($entityTypes); $this->_productTypeFactory->expects($this->once())->method('create')->willReturn($productTypeInstance); return $this; } @@ -489,12 +499,12 @@ public function testSaveProductAttributes() ] ]; $this->skuProcessor->expects($this->once()) - ->method('getNewSku') - ->with($testSku) - ->willReturn(['entity_id' => self::ENTITY_ID]); + ->method('getNewSku') + ->with($testSku) + ->willReturn(['entity_id' => self::ENTITY_ID]); $this->_connection->expects($this->any()) - ->method('quoteInto') - ->willReturnCallback([$this, 'returnQuoteCallback']); + ->method('quoteInto') + ->willReturnCallback([$this, 'returnQuoteCallback']); $this->_connection ->expects($this->once()) ->method('delete') @@ -515,8 +525,8 @@ public function testSaveProductAttributes() 'value' => $attributesData[$testTable][$testSku][$attributeId][$storeId], ]; $this->_connection->expects($this->once()) - ->method('insertOnDuplicate') - ->with($testTable, $tableData, ['value']); + ->method('insertOnDuplicate') + ->with($testTable, $tableData, ['value']); $object = $this->invokeMethod($this->importProduct, '_saveProductAttributes', [$attributesData]); $this->assertEquals($this->importProduct, $object); } @@ -531,6 +541,8 @@ public function testIsAttributeValidAssertAttrValid($attrParams, $rowData) $string = $this->getMockBuilder('\Magento\Framework\Stdlib\StringUtils')->setMethods(null)->getMock(); $this->setPropertyValue($this->importProduct, 'string', $string); + $this->validator->expects($this->once())->method('isAttributeValid')->willReturn(true); + $result = $this->importProduct->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); $this->assertTrue($result); } @@ -545,88 +557,14 @@ public function testIsAttributeValidAssertAttrInvalid($attrParams, $rowData) $string = $this->getMockBuilder('\Magento\Framework\Stdlib\StringUtils')->setMethods(null)->getMock(); $this->setPropertyValue($this->importProduct, 'string', $string); + $this->validator->expects($this->once())->method('isAttributeValid')->willReturn(false); + $messages = ['validator message']; + $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); + $result = $this->importProduct->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); $this->assertFalse($result); } - public function testIsAttributeValidNotValidAddErrorCall() - { - $attrCode = 'code'; - $attrParams = [ - 'type' => 'decimal', - ]; - $rowData = [ - $attrCode => 'incorrect' - ]; - $rowNum = 0; - - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError']) - ->getMock(); - $importProduct->expects($this->once())->method('addRowError'); - - $importProduct->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); - } - - public function testIsAttributeValidOnDuplicateAddErrorCall() - { - $attrCode = 'code'; - $attrCodeVal = 1000; - $expectedSkuVal = 'sku_val'; - $testSkuVal = 'some_sku'; - $attrParams = [ - 'type' => 'decimal', - 'is_unique' => true, - ]; - $rowData = [ - $attrCode => $attrCodeVal, - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $expectedSkuVal - ]; - $rowNum = 0; - - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError']) - ->getMock(); - $importProduct->expects($this->once())->method('addRowError'); - $this->setPropertyValue($importProduct, '_uniqueAttributes', [ - $attrCode => [$attrCodeVal => $testSkuVal] - ]); - - $importProduct->expects($this->once())->method('addRowError'); - - $return = $importProduct->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); - - $this->assertFalse($return); - } - - public function testIsAttributeValidAddIntoUniqueueAttributes() - { - $attrCode = 'code'; - $attrCodeVal = 1000; - $expectedSkuVal = 'sku_val'; - $attrParams = [ - 'type' => 'decimal', - 'is_unique' => true, - ]; - $rowData = [ - $attrCode => $attrCodeVal, - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $expectedSkuVal - ]; - $rowNum = 0; - - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); - - $importProduct->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); - - $_uniqueAttributes = $this->getPropertyValue($importProduct, '_uniqueAttributes'); - $this->assertEquals($expectedSkuVal, $_uniqueAttributes[$attrCode][$rowData[$attrCode]]); - } - public function testGetMultipleValueSeparatorDefault() { $this->setPropertyValue($this->importProduct, '_parameters', null); @@ -703,15 +641,14 @@ public function testGetRowScope($rowData, $expectedResult) } /** - * @dataProvider validateRowIsAlreadyValidatedDataProvider + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function testValidateRowIsAlreadyValidated($isInvalidRow, $expectedResult) + public function testValidateRowIsAlreadyValidated() { $rowNum = 0; $this->setPropertyValue($this->importProduct, '_validatedRows', [$rowNum => true]); - $this->setPropertyValue($this->importProduct, '_invalidRows', [$rowNum => $isInvalidRow]); $result = $this->importProduct->validateRow([], $rowNum); - $this->assertEquals($expectedResult, $result); + $this->assertTrue($result); } /** @@ -721,12 +658,15 @@ public function testValidateRowDeleteBehaviour($rowScope, $oldSku, $expectedResu { $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope']) + ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) ->getMock(); $importProduct ->expects($this->once()) ->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); + $importProduct + ->method('getErrorAggregator') + ->willReturn($this->getErrorAggregatorObject()); $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; $rowData = [ @@ -746,9 +686,9 @@ public function testValidateRowDeleteBehaviourAddRowErrorCall() ->getMock(); $importProduct->expects($this->once())->method('getBehavior') - ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); + ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); $importProduct->expects($this->once())->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); + ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); $importProduct->expects($this->once())->method('addRowError'); $rowData = [ \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', @@ -759,25 +699,13 @@ public function testValidateRowDeleteBehaviourAddRowErrorCall() public function testValidateRowValidatorCheck() { - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); - - $this->validator->expects($this->once())->method('isValid')->willReturn(false); $messages = ['validator message']; $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); - $importProduct->expects($this->at(0))->method('addRowError')->with($messages[0]); - $this->setPropertyValue($importProduct, 'validator', $this->validator); - //suppress option validation - $this->_rewriteGetOptionEntityInImportProduct($importProduct); $rowData = [ \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', ]; $rowNum = 0; - $this->setPropertyValue($importProduct, '_invalidRows', [$rowNum => '']); - - $importProduct->validateRow($rowData, $rowNum); + $this->importProduct->validateRow($rowData, $rowNum); } /** @@ -874,7 +802,7 @@ public function getStoreIdByCodeDataProvider() [ '$storeCode' => null, '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, - ], + ], [ '$storeCode' => 'value', '$expectedResult' => 'getStoreCodeToId value', @@ -887,10 +815,10 @@ public function getStoreIdByCodeDataProvider() */ public function testValidateRowCheckSpecifiedSku($sku, $expectedError) { - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity', 'getRowScope']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + [ 'addRowError', 'getOptionEntity', 'getRowScope'], + ['isRowInvalid' => true] + ); $rowNum = 0; $rowData = [ @@ -917,8 +845,11 @@ public function testValidateRowProcessEntityIncrement() { $count = 0; $rowNum = 0; + $errorAggregator = $this->getErrorAggregatorObject(['isRowInvalid']); + $errorAggregator->method('isRowInvalid')->willReturn(true); $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); - $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => '']; + $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); + $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; //suppress validator $this->_setValidatorMockInImportProduct($this->importProduct); $this->importProduct->validateRow($rowData, $rowNum); @@ -927,10 +858,11 @@ public function testValidateRowProcessEntityIncrement() public function testValidateRowValidateExistingProductTypeAddNewSku() { - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + [ 'addRowError', 'getOptionEntity'], + ['isRowInvalid' => true] + ); + $sku = 'sku'; $rowNum = 0; $rowData = [ @@ -982,10 +914,10 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() 'type_id' => 'type_id_val', ], ]; - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + ['addRowError', 'getOptionEntity'], + ['isRowInvalid' => true] + ); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $importProduct->expects($this->once())->method('addRowError')->with( @@ -1010,10 +942,7 @@ public function testValidateRowValidateExistingProductTypeResetSku() 'type_id' => 'type_id_val', ], ]; - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator(['getOptionEntity']); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); @@ -1030,8 +959,8 @@ public function testValidateRowValidateExistingProductTypeResetSku() $this->skuProcessor->expects($this->once())->method('getNewSku')->with($expectedSku)->willReturn($newSku); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); $productType = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $this->setPropertyValue($importProduct, '_productTypeModels', [ $newSku['type_id'] => $productType ]); @@ -1065,11 +994,10 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $oldSku = [ $sku => null, ]; - - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + ['addRowError', 'getOptionEntity'], + ['isRowInvalid' => true] + ); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $this->setPropertyValue($importProduct, '_productTypeModels', $_productTypeModels); @@ -1110,10 +1038,10 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() $_attrSetNameToId[$rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET]], 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value ]; - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + ['addRowError', 'getOptionEntity'], + ['isRowInvalid' => true] + ); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $this->setPropertyValue($importProduct, '_productTypeModels', $_productTypeModels); @@ -1130,15 +1058,110 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() public function testValidateRowValidateNewProductTypeResetSku() { - $this->markTestSkipped( - 'No chance to assert sku resetting due to mutually exclusive condition: - !isset($this->_invalidRows[$rowNum]) and isset($this->_invalidRows[$rowNum]) should be true simultaneously' + $sku = 'sku'; + $rowNum = 0; + $rowData = [ + \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', + \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', + ]; + + $oldSku = [ + $sku => [ + 'type_id' => 'type_id_val', + ], + ]; + $_productTypeModels = [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', + ]; + $_attrSetNameToId = [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' + ]; + + $importProduct = $this->createModelMockWithErrorAggregator( + ['addRowError', 'getOptionEntity'], + ['isRowInvalid' => false] ); + + $this->setPropertyValue($importProduct, '_oldSku', $oldSku); + $this->setPropertyValue($importProduct, '_productTypeModels', $_productTypeModels); + $this->setPropertyValue($importProduct, '_attrSetNameToId', $_attrSetNameToId); + + $this->_rewriteGetOptionEntityInImportProduct($importProduct);//suppress option validation + $this->_setValidatorMockInImportProduct($importProduct);//suppress validator + + $expectedSku = false; + $newSku = [ + 'attr_set_code' => 'new_attr_set_code', + 'type_id' => 'new_type_id_val', + ]; + $this->skuProcessor->expects($this->once())->method('getNewSku')->with($expectedSku)->willReturn($newSku); + $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); + $productType = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType') + ->disableOriginalConstructor() + ->getMock(); + $this->setPropertyValue($importProduct, '_productTypeModels', [ + $newSku['type_id'] => $productType + ]); + + $importProduct->validateRow($rowData, $rowNum); } public function testValidateDefaultScopeNotValidAttributesResetSku() { - $this->markTestSkipped('No chance to assert sku resetting because it is not used later in method.'); + $sku = 'sku'; + $rowNum = 0; + $attrCode = 'code'; + $stringUtilsMock = $this->getMockBuilder('\Magento\Framework\Stdlib\StringUtils')->setMethods(null)->getMock(); + $this->setPropertyValue($this->importProduct, 'string', $stringUtilsMock); + + $scopeMock = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Product', + ['getRowScope'], + [], + '', + false + ); + + $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; + $scopeRowData = [ + $sku => 'sku', + $colStore => null, + ]; + $scopeResult = \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT; + $scopeMock->expects($this->any())->method('getRowScope')->with($scopeRowData)->willReturn($scopeResult); + $oldSku = [ + $sku => [ + 'type_id' => 'type_id_val', + ], + ]; + + $this->setPropertyValue($this->importProduct, '_oldSku', $oldSku); + + $expectedSku = false; + $newSku = [ + 'attr_set_code' => 'new_attr_set_code', + 'type_id' => 'new_type_id_val', + ]; + $this->skuProcessor->expects($this->any())->method('getNewSku')->with($expectedSku)->willReturn($newSku); + $this->setPropertyValue($this->importProduct, 'skuProcessor', $this->skuProcessor); + + $attrParams = [ + 'type' => 'varchar', + ]; + $attrRowData = [ + 'code' => str_repeat( + 'a', + \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 + ), + ]; + + $this->validator->expects($this->once())->method('isAttributeValid')->willReturn(false); + $messages = ['validator message']; + $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); + + $result = $this->importProduct->isAttributeValid($attrCode, $attrParams, $attrRowData, $rowNum); + $this->assertFalse($result); } public function testValidateRowSetAttributeSetCodeIntoRowData() @@ -1163,18 +1186,15 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() 'type_id' => 'type_id_val', ], ]; - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator(['getOptionEntity']); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $this->skuProcessor->expects($this->any())->method('getNewSku')->willReturn($newSku); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); $productType = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $productType->expects($this->once())->method('isRowValid')->with($expectedRowData); $this->setPropertyValue($importProduct, '_productTypeModels', [ $newSku['type_id'] => $productType @@ -1201,16 +1221,15 @@ public function testValidateValidateOptionEntity() 'type_id' => 'type_id_val', ], ]; - $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') - ->disableOriginalConstructor() - ->setMethods(['addRowError', 'getOptionEntity']) - ->getMock(); + $importProduct = $this->createModelMockWithErrorAggregator( + ['addRowError', 'getOptionEntity'], + ['isRowInvalid' => true] + ); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); //suppress validator $this->_setValidatorMockInImportProduct($importProduct); - $this->setPropertyValue($importProduct, '_invalidRows', [0 => '']); $option = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product\Option') ->disableOriginalConstructor() @@ -1459,7 +1478,7 @@ public function getRowScopeDataProvider() $colSku => null, $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_NULL + '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE ], [ '$rowData' => [ @@ -1478,23 +1497,6 @@ public function getRowScopeDataProvider() ]; } - /** - * @return array - */ - public function validateRowIsAlreadyValidatedDataProvider() - { - return [ - [ - '$isInvalidRow' => true, - '$expectedResult' => false, - ], - [ - '$isInvalidRow' => null, - '$expectedResult' => true, - ], - ]; - } - /** * @return mixed */ @@ -1577,7 +1579,6 @@ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) $this->_rewriteGetOptionEntityInImportProduct($importProduct); //suppress validator $this->_setValidatorMockInImportProduct($importProduct); - $this->setPropertyValue($importProduct, '_invalidRows', [0 => '']); return $importProduct; } @@ -1614,4 +1615,26 @@ private function _rewriteGetOptionEntityInImportProduct($importProduct) return $importProduct; } + + /** + * @param array $methods + * @param array $errorAggregatorMethods + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) + { + $methods[] = 'getErrorAggregator'; + $importProduct = $this->getMockBuilder('\Magento\CatalogImportExport\Model\Import\Product') + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + $errorMethods = array_keys($errorAggregatorMethods); + $errorAggregator = $this->getErrorAggregatorObject($errorMethods); + foreach ($errorAggregatorMethods as $method => $result) { + $errorAggregator->method($method)->willReturn($result); + } + $importProduct->method('getErrorAggregator')->willReturn($errorAggregator); + + return $importProduct; + } } diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml index 7229759817235..55daa1e2d6f1f 100644 --- a/app/code/Magento/CatalogImportExport/etc/di.xml +++ b/app/code/Magento/CatalogImportExport/etc/di.xml @@ -24,6 +24,7 @@ Magento\CatalogImportExport\Model\Import\Product\Validator\TierPrice Magento\CatalogImportExport\Model\Import\Product\Validator\Website Magento\CatalogImportExport\Model\Import\Product\Validator\Weight + Magento\CatalogImportExport\Model\Import\Product\Validator\Quantity diff --git a/app/code/Magento/CatalogImportExport/etc/import.xml b/app/code/Magento/CatalogImportExport/etc/import.xml index fb6028af373d8..be9807d914866 100644 --- a/app/code/Magento/CatalogImportExport/etc/import.xml +++ b/app/code/Magento/CatalogImportExport/etc/import.xml @@ -8,7 +8,7 @@ - + diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index c2ffb1dae3b6e..4401ee36fc722 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -5,6 +5,9 @@ * Copyright © 2015 Magento. All rights reserved. * See COPYING.txt for license details. */ + +// @codingStandardsIgnoreFile + namespace Magento\ConfigurableImportExport\Model\Import\Product\Type; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; @@ -26,6 +29,8 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ const ERROR_INVALID_WEBSITE = 'invalidSuperAttrWebsite'; + const ERROR_DUPLICATED_VARIATIONS = 'duplicatedVariations'; + /** * Validation failure message template definitions * @@ -35,6 +40,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ self::ERROR_ATTRIBUTE_CODE_IS_NOT_SUPER => 'Attribute with this code is not super', self::ERROR_INVALID_OPTION_VALUE => 'Invalid option value', self::ERROR_INVALID_WEBSITE => 'Invalid website code for super attribute', + self::ERROR_DUPLICATED_VARIATIONS => 'SKU %s contains duplicated variations', ]; /** @@ -613,7 +619,8 @@ protected function _collectSuperData($rowData) if ($this->_getSuperAttributeId($productId, $attrParams['id'])) { $productSuperAttrId = $this->_getSuperAttributeId($productId, $attrParams['id']); } elseif (isset($this->_superAttributesData['attributes'][$productId][$attrParams['id']])) { - $productSuperAttrId = $this->_superAttributesData['attributes'][$productId][$attrParams['id']]['product_super_attribute_id']; + $attributes = $this->_superAttributesData['attributes']; + $productSuperAttrId = $attributes[$productId][$attrParams['id']]['product_super_attribute_id']; $this->_collectSuperDataLabels($data, $productSuperAttrId, $productId, $variationLabels); } else { $productSuperAttrId = $this->_getNextAttrId(); @@ -780,13 +787,21 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) { $error = false; $dataWithExtraVirtualRows = $this->_parseVariations($rowData); + $skus = []; if (!empty($dataWithExtraVirtualRows)) { array_unshift($dataWithExtraVirtualRows, $rowData); } else { $dataWithExtraVirtualRows[] = $rowData; } - foreach ($dataWithExtraVirtualRows as $data) { - $error |= !parent::isRowValid($data, $rowNum, $isNewProduct); + foreach ($dataWithExtraVirtualRows as $option) { + if (isset($option['_super_products_sku'])) { + if (in_array($option['_super_products_sku'], $skus)) { + $error = true; + $this->_entityModel->addRowError(sprintf($this->_messageTemplates[self::ERROR_DUPLICATED_VARIATIONS], $option['_super_products_sku']), $rowNum); + } + $skus[] = $option['_super_products_sku']; + } + $error |= !parent::isRowValid($option, $rowNum, $isNewProduct); } return !$error; } diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php index 5fa964ca653bd..019adef514f78 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php @@ -6,7 +6,6 @@ namespace Magento\ConfigurableImportExport\Test\Unit\Model\Import\Product\Type; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use \Magento\ConfigurableImportExport; /** @@ -14,14 +13,11 @@ * @package Magento\ConfigurableImportExport\Test\Unit\Model\Import\Product\Type * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ConfigurableTest extends \PHPUnit_Framework_TestCase +class ConfigurableTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** @var ConfigurableImportExport\Model\Import\Product\Type\Configurable */ protected $configurable; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - /** * @var \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ @@ -81,6 +77,8 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { + parent::setUp(); + $this->setCollectionFactory = $this->getMock( 'Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory', ['create'], @@ -158,18 +156,20 @@ protected function setUp() 'isRowAllowedToImport', 'getConnection', 'getAttrSetIdToName', + 'getErrorAggregator', 'getAttributeOptions' ], [], '', false ); + $this->_entityModel->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); + $this->params = [ 0 => $this->_entityModel, 1 => 'configurable' ]; - $this->objectManagerHelper = new ObjectManagerHelper($this); $this->_connection = $this->getMock( 'Magento\Framework\DB\Adapter\Pdo\Mysql', diff --git a/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php b/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php index df00c55a6955e..87b29e5e6867e 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php @@ -6,6 +6,7 @@ namespace Magento\CustomerImportExport\Model\Import; use Magento\CustomerImportExport\Model\Resource\Import\Customer\Storage; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Import entity abstract customer model @@ -23,6 +24,11 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit const COLUMN_EMAIL = '_email'; + const COLUMN_DEFAULT_BILLING = 'default_billing'; + + const COLUMN_DEFAULT_SHIPPING = 'default_shipping'; + + /**#@-*/ /**#@+ @@ -49,7 +55,8 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit * * @var string[] */ - protected $_ignoredAttributes = ['website_id', 'store_id', 'default_billing', 'default_shipping']; + protected $_ignoredAttributes = ['website_id', 'store_id', + self::COLUMN_DEFAULT_BILLING, self::COLUMN_DEFAULT_SHIPPING]; /** * Customer collection wrapper @@ -63,6 +70,13 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit */ protected $_storageFactory; + /** + * If we should check column names + * + * @var bool + */ + protected $needColumnCheck = true; + /** * {@inheritdoc} */ @@ -74,6 +88,7 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory * @param \Magento\Eav\Model\Config $eavConfig @@ -87,6 +102,7 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\App\Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, @@ -100,6 +116,7 @@ public function __construct( $importFactory, $resourceHelper, $resource, + $errorAggregator, $storeManager, $collectionFactory, $eavConfig, @@ -168,7 +185,7 @@ public function validateRow(array $rowData, $rowNumber) { if (isset($this->_validatedRows[$rowNumber])) { // check that row is already validated - return !isset($this->_invalidRows[$rowNumber]); + return !$this->getErrorAggregator()->isRowInvalid($rowNumber); } $this->_validatedRows[$rowNumber] = true; $this->_processedEntitiesCount++; @@ -178,7 +195,7 @@ public function validateRow(array $rowData, $rowNumber) $this->_validateRowForDelete($rowData, $rowNumber); } - return !isset($this->_invalidRows[$rowNumber]); + return !$this->getErrorAggregator()->isRowInvalid($rowNumber); } /** @@ -222,7 +239,7 @@ protected function _checkUniqueKey(array $rowData, $rowNumber) $this->addRowError(static::ERROR_INVALID_WEBSITE, $rowNumber, static::COLUMN_WEBSITE); } } - return !isset($this->_invalidRows[$rowNumber]); + return !$this->getErrorAggregator()->isRowInvalid($rowNumber); } /** diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index d0eef32fc186a..712e13fd0268d 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -5,6 +5,8 @@ */ namespace Magento\CustomerImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; + /** * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -212,12 +214,29 @@ class Address extends AbstractCustomer */ protected $dateTime; + /** + * Customer attributes + * + * @var string[] + */ + protected $_customerAttributes = []; + + /** + * Valid column names + * + * @array + */ + protected $validColumnNames = [ + "region_id", "vat_is_valid", "vat_request_date", "vat_request_id", "vat_request_success" + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory * @param \Magento\Eav\Model\Config $eavConfig @@ -238,6 +257,7 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\App\Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, @@ -268,6 +288,7 @@ public function __construct( $importFactory, $resourceHelper, $resource, + $errorAggregator, $storeManager, $collectionFactory, $eavConfig, @@ -374,6 +395,7 @@ protected function _initCountryRegions() * * @abstract * @return boolean + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _importData() { @@ -387,7 +409,11 @@ protected function _importData() foreach ($bunch as $rowNumber => $rowData) { // check row data - if (!$this->validateRow($rowData, $rowNumber)) { + if ($this->_isOptionalAddressEmpty($rowData) || !$this->validateRow($rowData, $rowNumber)) { + continue; + } + if ($this->getErrorAggregator()->hasToBeTerminated()) { + $this->getErrorAggregator()->addRowToSkip($rowNumber); continue; } @@ -405,6 +431,7 @@ protected function _importData() $deleteRowIds[] = $rowData[self::COLUMN_ADDRESS_ID]; } } + $this->updateItemsCounterStats($newRows, $updateRows, $deleteRowIds); $this->_saveAddressEntities( $newRows, @@ -648,6 +675,32 @@ public static function getDefaultAddressAttributeMapping() return self::$_defaultAddressAttributeMapping; } + /** + * check if address for import is empty (for customer composite mode) + * + * @param array $rowData + * @return array + */ + protected function _isOptionalAddressEmpty(array $rowData) + { + if (empty($this->_customerAttributes)) { + return false; + } + unset( + $rowData[Customer::COLUMN_WEBSITE], + $rowData[Customer::COLUMN_STORE], + $rowData['_email'] + ); + + foreach ($rowData as $key => $value) { + if (!in_array($key, $this->_customerAttributes) && !empty($value)) { + return false; + } + } + + return true; + } + /** * Validate row for add/update action * @@ -755,4 +808,16 @@ protected function _checkRowDuplicate($customerId, $addressId) return false; } } + + /** + * set customer attributes + * + * @param array $customerAttributes + * @return $this + */ + public function setCustomerAttributes($customerAttributes) + { + $this->_customerAttributes = $customerAttributes; + return $this; + } } diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index a92fb906bd264..f980fb1eaca3e 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -5,6 +5,8 @@ */ namespace Magento\CustomerImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -25,6 +27,8 @@ class Customer extends AbstractCustomer const COLUMN_STORE = '_store'; + const COLUMN_PASSWORD = 'password'; + /**#@-*/ /**#@+ @@ -117,12 +121,47 @@ class Customer extends AbstractCustomer */ protected $masterAttributeCode = 'email'; + /** + * Valid column names + * + * @array + */ + protected $validColumnNames = [ + self::COLUMN_DEFAULT_BILLING, + self::COLUMN_DEFAULT_SHIPPING, + self::COLUMN_PASSWORD, + ]; + + /** + * Customer fields in file + */ + public $customerFields = [ + 'group_id', + 'store_id', + 'updated_at', + 'created_at', + 'created_in', + 'prefix', + 'firstname', + 'middlename', + 'lastname', + 'suffix', + 'dob', + 'password_hash', + 'taxvat', + 'confirmation', + 'gender', + 'rp_token', + 'rp_token_created_at', + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory * @param \Magento\Eav\Model\Config $eavConfig @@ -138,6 +177,7 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\App\Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, @@ -163,6 +203,7 @@ public function __construct( $importFactory, $resourceHelper, $resource, + $errorAggregator, $storeManager, $collectionFactory, $eavConfig, @@ -196,6 +237,11 @@ public function __construct( $this->_initStores(true)->_initAttributes(); + $this->validColumnNames = array_merge( + $this->validColumnNames, + $this->customerFields + ); + $this->_customerModel = $customerFactory->create(); /** @var $customerResource \Magento\Customer\Model\Resource\Customer */ $customerResource = $this->_customerModel->getResource(); @@ -215,30 +261,11 @@ protected function _saveCustomerEntities(array $entitiesToCreate, array $entitie $this->_connection->insertMultiple($this->_entityTable, $entitiesToCreate); } - $customerFields = [ - 'group_id', - 'store_id', - 'updated_at', - 'created_at', - 'created_in', - 'prefix', - 'firstname', - 'middlename', - 'lastname', - 'suffix', - 'dob', - 'password_hash', - 'taxvat', - 'confirmation', - 'gender', - 'rp_token', - 'rp_token_created_at', - ]; if ($entitiesToUpdate) { $this->_connection->insertOnDuplicate( $this->_entityTable, $entitiesToUpdate, - $customerFields + $this->customerFields ); } @@ -395,6 +422,7 @@ protected function _prepareDataForUpdate(array $rowData) * * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _importData() { @@ -408,6 +436,10 @@ protected function _importData() if (!$this->validateRow($rowData, $rowNumber)) { continue; } + if ($this->getErrorAggregator()->hasToBeTerminated()) { + $this->getErrorAggregator()->addRowToSkip($rowNumber); + continue; + } if ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE) { $entitiesToDelete[] = $this->_getCustomerId( @@ -429,6 +461,7 @@ protected function _importData() } } } + $this->updateItemsCounterStats($entitiesToCreate, $entitiesToUpdate, $entitiesToDelete); /** * Save prepared data */ diff --git a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php index 5ab9b3e042b13..cd9e70e7fe225 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php @@ -5,6 +5,8 @@ */ namespace Magento\CustomerImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; + /** * Import entity customer combined model * @@ -123,6 +125,24 @@ class CustomerComposite extends \Magento\ImportExport\Model\Import\AbstractEntit */ protected $_dataSourceModels; + /** + * If we should check column names + * + * @var bool + */ + protected $needColumnCheck = true; + + /** + * Valid column names + * + * @array + */ + protected $validColumnNames = [ + Customer::COLUMN_DEFAULT_BILLING, + Customer::COLUMN_DEFAULT_SHIPPING, + Customer::COLUMN_PASSWORD, + ]; + /** * {@inheritdoc} */ @@ -134,10 +154,12 @@ class CustomerComposite extends \Magento\ImportExport\Model\Import\AbstractEntit * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\CustomerImportExport\Model\Resource\Import\CustomerComposite\DataFactory $dataFactory * @param \Magento\CustomerImportExport\Model\Import\CustomerFactory $customerFactory * @param \Magento\CustomerImportExport\Model\Import\AddressFactory $addressFactory * @param array $data + * @throws \Magento\Framework\Exception\LocalizedException * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -147,12 +169,13 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\App\Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\CustomerImportExport\Model\Resource\Import\CustomerComposite\DataFactory $dataFactory, \Magento\CustomerImportExport\Model\Import\CustomerFactory $customerFactory, \Magento\CustomerImportExport\Model\Import\AddressFactory $addressFactory, array $data = [] ) { - parent::__construct($string, $scopeConfig, $importFactory, $resourceHelper, $resource, $data); + parent::__construct($string, $scopeConfig, $importFactory, $resourceHelper, $resource, $errorAggregator, $data); $this->addMessageTemplate( self::ERROR_ROW_IS_ORPHAN, @@ -201,6 +224,13 @@ public function __construct( } $this->_initAddressAttributes(); + $this->validColumnNames = array_merge( + $this->validColumnNames, + $this->_customerAttributes, + $this->_addressAttributes, + $this->_customerEntity->customerFields + ); + // next customer id if (isset($data['next_customer_id'])) { $this->_nextCustomerId = $data['next_customer_id']; @@ -248,7 +278,7 @@ protected function _importData() { $result = $this->_customerEntity->importData(); if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE) { - return $result && $this->_addressEntity->importData(); + return $result && $this->_addressEntity->setCustomerAttributes($this->_customerAttributes)->importData(); } return $result; @@ -417,53 +447,6 @@ public function setSource(\Magento\ImportExport\Model\Import\AbstractSource $sou return parent::setSource($source); } - /** - * Returns error information grouped by error types and translated (if possible) - * - * @return array - */ - public function getErrorMessages() - { - $errors = $this->_customerEntity->getErrorMessages(); - $addressErrors = $this->_addressEntity->getErrorMessages(); - foreach ($addressErrors as $message => $rowNumbers) { - if (isset($errors[$message])) { - foreach ($rowNumbers as $rowNumber) { - $errors[$message][] = $rowNumber; - } - $errors[$message] = array_unique($errors[$message]); - } else { - $errors[$message] = $rowNumbers; - } - } - - return array_merge($errors, parent::getErrorMessages()); - } - - /** - * Returns error counter value - * - * @return int - */ - public function getErrorsCount() - { - return $this->_customerEntity->getErrorsCount() + - $this->_addressEntity->getErrorsCount() + - parent::getErrorsCount(); - } - - /** - * Returns invalid rows count - * - * @return int - */ - public function getInvalidRowsCount() - { - return $this->_customerEntity->getInvalidRowsCount() + - $this->_addressEntity->getInvalidRowsCount() + - parent::getInvalidRowsCount(); - } - /** * Returns number of checked entities * diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AbstractCustomerTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AbstractCustomerTest.php index ce747c47d4ea1..64a4161c3625c 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AbstractCustomerTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AbstractCustomerTest.php @@ -11,7 +11,7 @@ use Magento\CustomerImportExport\Model\Import\AbstractCustomer; -class AbstractCustomerTest extends \PHPUnit_Framework_TestCase +class AbstractCustomerTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * Abstract customer export model @@ -77,8 +77,15 @@ protected function _getModelMock() $modelMock = $this->getMockBuilder('Magento\CustomerImportExport\Model\Import\AbstractCustomer') ->disableOriginalConstructor() - ->setMethods(['_getCustomerCollection', '_validateRowForUpdate', '_validateRowForDelete']) - ->getMockForAbstractClass(); + ->setMethods( + [ + 'getErrorAggregator', + '_getCustomerCollection', + '_validateRowForUpdate', + '_validateRowForDelete' + ] + )->getMockForAbstractClass(); + $modelMock->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $property = new \ReflectionProperty($modelMock, '_websiteCodeToId'); $property->setAccessible(true); @@ -165,6 +172,7 @@ public function checkUniqueKeyDataProvider() * @param array $rowData * @param array $errors * @param boolean $isValid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function testCheckUniqueKey(array $rowData, array $errors, $isValid = false) { @@ -179,7 +187,6 @@ public function testCheckUniqueKey(array $rowData, array $errors, $isValid = fal } else { $this->assertFalse($checkUniqueKey->invoke($this->_model, $rowData, 0)); } - $this->assertAttributeEquals($errors, '_errors', $this->_model); } public function testValidateRowForUpdate() diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php index 5234e871462d8..5aa8f1c34cb05 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php @@ -105,6 +105,12 @@ class AddressTest extends \PHPUnit_Framework_TestCase */ protected $_objectManagerMock; + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface + * |\PHPUnit_Framework_MockObject_MockObject + */ + protected $errorAggregator; + /** * Init entity adapter model */ @@ -120,6 +126,13 @@ protected function setUp() ->method('getWebsites') ->will($this->returnCallback([$this, 'getWebsites'])); $this->_model = $this->_getModelMock(); + $this->errorAggregator = $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator', + ['hasToBeTerminated'], + [], + '', + false + ); } /** @@ -351,7 +364,8 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() '_saveAddressAttributes', '_saveCustomerDefaults', '_deleteAddressEntities', - '_mergeEntityAttributes' + '_mergeEntityAttributes', + 'getErrorAggregator' ], [], '', @@ -384,6 +398,9 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() // mock expects for entity adapter $modelMock->expects($this->any())->method('validateRow')->will($this->returnValue(true)); + $modelMock->expects($this->any()) + ->method('getErrorAggregator') + ->will($this->returnValue($this->errorAggregator)); $modelMock->expects($this->any())->method('_prepareDataForUpdate')->will($this->returnValue($updateResult)); @@ -426,6 +443,13 @@ protected function _getModelMock() $this->getMock('Magento\ImportExport\Model\ImportFactory', [], [], '', false), $this->getMock('Magento\ImportExport\Model\Resource\Helper', [], [], '', false), $this->getMock('Magento\Framework\App\Resource', [], [], '', false), + $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface', + [], + [], + '', + false + ), $this->_storeManager, $this->getMock('Magento\ImportExport\Model\Export\Factory', [], [], '', false), $this->getMock('Magento\Eav\Model\Config', [], [], '', false), @@ -440,13 +464,7 @@ protected function _getModelMock() $this->getMock('Magento\Directory\Model\Resource\Region\CollectionFactory', [], [], '', false), $this->getMock('Magento\Customer\Model\CustomerFactory', [], [], '', false), $this->getMock('Magento\Customer\Model\Resource\Address\CollectionFactory', [], [], '', false), - $this->getMock( - 'Magento\Customer\Model\Resource\Address\Attribute\CollectionFactory', - [], - [], - '', - false - ), + $this->getMock('Magento\Customer\Model\Resource\Address\Attribute\CollectionFactory', [], [], '', false), new \Magento\Framework\Stdlib\DateTime(), $this->_getModelDependencies() ); @@ -476,30 +494,6 @@ public function validateRowForUpdateDataProvider() '$errors' => [], '$isValid' => true, ], - 'no customer' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_update_no_customer.php', - '$errors' => [ - Address::ERROR_CUSTOMER_NOT_FOUND => [ - [1, null], - ], - ], - ], - 'absent required attribute' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_update_absent_required_attribute.php', - '$errors' => [ - Address::ERROR_VALUE_IS_REQUIRED => [ - [1, Address::COLUMN_COUNTRY_ID], - ], - ], - ], - 'invalid region' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_update_invalid_region.php', - '$errors' => [ - Address::ERROR_INVALID_REGION => [ - [1, Address::COLUMN_REGION], - ], - ], - ] ]; } @@ -516,30 +510,6 @@ public function validateRowForDeleteDataProvider() '$errors' => [], '$isValid' => true, ], - 'empty address id' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_delete_empty_address_id.php', - '$errors' => [ - Address::ERROR_ADDRESS_ID_IS_EMPTY => [ - [1, null], - ], - ], - ], - 'invalid address' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_delete_address_not_found.php', - '$errors' => [ - Address::ERROR_ADDRESS_NOT_FOUND => [ - [1, null], - ], - ], - ], - 'no customer' => [ - '$rowData' => include __DIR__ . '/_files/row_data_address_delete_no_customer.php', - '$errors' => [ - Address::ERROR_CUSTOMER_NOT_FOUND => [ - [1, null], - ], - ], - ] ]; } @@ -549,6 +519,7 @@ public function validateRowForDeleteDataProvider() * @param array $rowData * @param array $errors * @param boolean $isValid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function testValidateRowForUpdate(array $rowData, array $errors, $isValid = false) { @@ -559,7 +530,6 @@ public function testValidateRowForUpdate(array $rowData, array $errors, $isValid } else { $this->assertFalse($this->_model->validateRow($rowData, 0)); } - $this->assertAttributeEquals($errors, '_errors', $this->_model); } /** @@ -575,7 +545,7 @@ public function testValidateRowForUpdateDuplicateRows() $this->_model->setParameters(['behavior' => $behavior]); - $secondRow = $firstRow = [ + $firstRow = [ '_website' => 'website1', '_email' => 'test1@email.com', '_entity_id' => '1', @@ -601,16 +571,7 @@ public function testValidateRowForUpdateDuplicateRows() '_address_default_billing_' => '1', '_address_default_shipping_' => '1', ]; - $secondRow['postcode'] = '90210'; - - $errors = [ - Address::ERROR_DUPLICATE_PK => [[2, null]], - ]; - $this->assertTrue($this->_model->validateRow($firstRow, 0)); - $this->assertFalse($this->_model->validateRow($secondRow, 1)); - - $this->assertAttributeEquals($errors, '_errors', $this->_model); } /** @@ -622,6 +583,7 @@ public function testValidateRowForUpdateDuplicateRows() * @param array $rowData * @param array $errors * @param boolean $isValid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function testValidateRowForDelete(array $rowData, array $errors, $isValid = false) { @@ -632,7 +594,6 @@ public function testValidateRowForDelete(array $rowData, array $errors, $isValid } else { $this->assertFalse($this->_model->validateRow($rowData, 0)); } - $this->assertAttributeEquals($errors, '_errors', $this->_model); } /** diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php index 085a36afdb834..a66a1bb84af1d 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php @@ -59,12 +59,12 @@ class CustomerCompositeTest extends \PHPUnit_Framework_TestCase protected $_dataFactory; /** - * @var CustomerFactory + * @var \Magento\CustomerImportExport\Model\Import\CustomerFactory */ protected $_customerFactory; /** - * @var AddressFactory + * @var \Magento\CustomerImportExport\Model\Import\AddressFactory */ protected $_addressFactory; @@ -73,6 +73,23 @@ class CustomerCompositeTest extends \PHPUnit_Framework_TestCase */ protected $_scopeConfigMock; + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface + * |\PHPUnit_Framework_MockObject_MockObject + */ + protected $errorAggregator; + + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError $newError + */ + protected $error; + + /** + * @var \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorFactory + * |\PHPUnit_Framework_MockObject_MockObject + */ + protected $errorFactory; + /** * Expected prepared data after method CustomerComposite::_prepareRowForDb * @@ -151,6 +168,33 @@ protected function setUp() false ); + $this->errorFactory = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorFactory', + ['create'], + [], + '', + false + ); + + $this->error = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', + ['init'], + [], + '', + false + ); + + $this->errorFactory->expects($this->any())->method('create')->will($this->returnValue($this->error)); + $this->error->expects($this->any())->method('init')->will($this->returnValue(true)); + + $this->errorAggregator = $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator', + ['hasToBeTerminated'], + [$this->errorFactory], + '', + true + ); + $this->_scopeConfigMock = $this->getMock('Magento\Framework\App\Config\ScopeConfigInterface'); } @@ -166,6 +210,7 @@ protected function _createModelMock($data) $this->_importFactory, $this->_resourceHelper, $this->_resource, + $this->errorAggregator, $this->_dataFactory, $this->_customerFactory, $this->_addressFactory, @@ -542,24 +587,6 @@ public function getRowDataProvider() '$expectedErrors' => [], '$behavior' => Import::BEHAVIOR_APPEND, ], - 'customer and addresses row with filed validation, append behavior' => [ - '$rows' => [ - [ - Customer::COLUMN_EMAIL => 'test@test.com', - Customer::COLUMN_WEBSITE => 'admin', - Address::COLUMN_ADDRESS_ID => null, - ], - [ - Customer::COLUMN_EMAIL => '', - Customer::COLUMN_WEBSITE => '', - Address::COLUMN_ADDRESS_ID => 1 - ], - ], - '$calls' => ['customerValidationCalls' => 1, 'addressValidationCalls' => 0], - '$validationReturn' => false, - '$expectedErrors' => ['Orphan rows that will be skipped due default row errors'], - '$behavior' => Import::BEHAVIOR_APPEND, - ] ]; } @@ -623,42 +650,6 @@ public function testSetSource() $modelUnderTest->setSource($source); } - public function testGetErrorMessages() - { - $errorMessages = [ - 'Required field' => [1, 2, 3], - 'Bad password' => [1], - 'Wrong website' => [1, 2], - ]; - $customerEntity = $this->_getCustomerEntityMock(); - $customerEntity->expects($this->once())->method('getErrorMessages')->will($this->returnValue($errorMessages)); - - $errorMessages = ['Required field' => [2, 3, 4, 5], 'Wrong address' => [1, 2]]; - $addressEntity = $this->_getAddressEntityMock(); - $addressEntity->expects($this->once())->method('getErrorMessages')->will($this->returnValue($errorMessages)); - - $data = $this->_getModelDependencies(); - $data['customer_entity'] = $customerEntity; - $data['address_entity'] = $addressEntity; - - $modelUnderTest = $this->_createModelMock($data); - - $modelUnderTest->addRowError('Bad password', 1); - - $expectedErrors = [ - 'Required field' => [1, 2, 3, 4, 5], - 'Bad password' => [2], - 'Wrong website' => [1, 2], - 'Wrong address' => [1, 2], - ]; - - $actualErrors = $modelUnderTest->getErrorMessages(); - foreach ($expectedErrors as $error => $rows) { - $this->assertArrayHasKey($error, $actualErrors); - $this->assertSame($rows, array_values($actualErrors[$error])); - } - } - public function testPrepareRowForDb() { $modelUnderTest = $this->_getModelMockForPrepareRowForDb(); @@ -764,26 +755,6 @@ public function testImportData($behavior, $customerImport, $addressImport, $resu } } - public function testGetErrorsCount() - { - $customerReturnData = 1; - $addressReturnData = 2; - $model = $this->_getModelForGetterTest('getErrorsCount', $customerReturnData, $addressReturnData); - $model->addRowError(CustomerComposite::ERROR_ROW_IS_ORPHAN, 1); - - $this->assertEquals($customerReturnData + $addressReturnData + 1, $model->getErrorsCount()); - } - - public function testGetInvalidRowsCount() - { - $customerReturnData = 3; - $addressReturnData = 2; - $model = $this->_getModelForGetterTest('getInvalidRowsCount', $customerReturnData, $addressReturnData); - $model->addRowError(CustomerComposite::ERROR_ROW_IS_ORPHAN, 1); - - $this->assertEquals($customerReturnData + $addressReturnData + 1, $model->getInvalidRowsCount()); - } - public function testGetProcessedEntitiesCount() { $customerReturnData = 3; diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php index 82a6a12863992..d59e5cc96dc35 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php @@ -91,9 +91,18 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() '_saveCustomerEntities', '_saveCustomerAttributes', '_deleteCustomerEntities', + 'getErrorAggregator', ]) ->getMock(); + $errorAggregator = $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator', + ['hasToBeTerminated'], + [], + '', + false + ); + $availableBehaviors = new \ReflectionProperty($modelMock, '_availableBehaviors'); $availableBehaviors->setAccessible(true); $availableBehaviors->setValue($modelMock, $this->_availableBehaviors); @@ -145,6 +154,10 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() ->method('_deleteCustomerEntities') ->will($this->returnCallback([$this, 'validateDeleteCustomerEntities'])); + $modelMock->expects($this->any()) + ->method('getErrorAggregator') + ->will($this->returnValue($errorAggregator)); + return $modelMock; } diff --git a/app/code/Magento/DownloadableImportExport/Helper/Data.php b/app/code/Magento/DownloadableImportExport/Helper/Data.php new file mode 100644 index 0000000000000..04a31a817300e --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Helper/Data.php @@ -0,0 +1,98 @@ +fileHelper = $fileHelper; + $this->fileUploader = $uploaderFactory->create(); + $this->fileUploader->init(); + $this->fileUploader->setAllowedExtensions($this->getAllowedExtensions()); + $this->fileUploader->removeValidateCallback('catalog_product_image'); + $this->connection = $resource->getConnection('write'); + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + } + + /** + * Returns an object for upload a media files + * + * @param string $type + * @param array $parameters + * @return \Magento\CatalogImportExport\Model\Import\Uploader + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getUploader($type, $parameters) + { + $dirConfig = DirectoryList::getDefaultConfig(); + $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + + $DS = DIRECTORY_SEPARATOR; + + if (!empty($parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR])) { + $tmpPath = $parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR]; + } else { + $tmpPath = $dirAddon . $DS . $this->mediaDirectory->getRelativePath('import'); + } + + if (!$this->fileUploader->setTmpDir($tmpPath)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('File directory \'%1\' is not readable.', $tmpPath) + ); + } + $destinationDir = "downloadable/files/" . $type; + $destinationPath = $dirAddon . $DS . $this->mediaDirectory->getRelativePath($destinationDir); + + $this->mediaDirectory->create($destinationDir); + if (!$this->fileUploader->setDestDir($destinationPath)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('File directory \'%1\' is not writable.', $destinationPath) + ); + } + return $this->fileUploader; + } + + /** + * Get all allowed extensions + * + * @return array + */ + protected function getAllowedExtensions() + { + $result = []; + foreach (array_keys($this->fileHelper->getAllMineTypes()) as $option) { + $result[] = substr($option, 1); + } + return $result; + } +} diff --git a/app/code/Magento/DownloadableImportExport/LICENSE.txt b/app/code/Magento/DownloadableImportExport/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/DownloadableImportExport/LICENSE_AFL.txt b/app/code/Magento/DownloadableImportExport/LICENSE_AFL.txt new file mode 100644 index 0000000000000..87943b95d43a5 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php new file mode 100644 index 0000000000000..f8132f26e1e96 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php @@ -0,0 +1,856 @@ + 'Options for downloadable products not found', + self::ERROR_GROUP_TITLE_NOT_FOUND => 'Group titles not found for downloadable products', + self::ERROR_OPTION_NO_TITLE => 'Option no title', + self::ERROR_MOVE_FILE => 'Error move file', + self::ERROR_COLS_IS_EMPTY => 'Missing sample and links data for the downloadable product' + ]; + + /** + * Entity model parameters. + * + * @var array + */ + protected $parameters = []; + + /** + * Ids products + * + * @var array + */ + protected $productIds = []; + + /** + * Array of cached options. + * + * @var array + */ + protected $cachedOptions = [ + 'link' => [], + 'sample' => [] + ]; + + /** + * Instance of empty sample + * + * @var array + */ + protected $dataSample = [ + 'sample_id' => null, + 'product_id' => null, + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null, + 'sort_order' => self::DEFAULT_SORT_ORDER + ]; + + /** + * Instance of empty sample title + * + * @var array + */ + protected $dataSampleTitle = [ + 'sample_id' => null, + 'store_id' => Store::DEFAULT_STORE_ID, + 'title' => null + ]; + + /** + * Instance of empty link + * + * @var array + */ + protected $dataLink = [ + 'link_id' => null, + 'product_id' => null, + 'sort_order' => self::DEFAULT_SORT_ORDER, + 'number_of_downloads' => self::DEFAULT_NUMBER_OF_DOWNLOADS, + 'is_shareable' => self::DEFAULT_IS_SHAREABLE, + 'link_url' => null, + 'link_file' => null, + 'link_type' => null, + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null + ]; + + /** + * Instance of empty link title + * + * @var array + */ + protected $dataLinkTitle = [ + 'link_id' => null, + 'store_id' => Store::DEFAULT_STORE_ID, + 'title' => null + ]; + + /** + * Instance of empty link price + * + * @var array + */ + protected $dataLinkPrice = [ + 'price_id' => null, + 'link_id' => null, + 'website_id' => self::DEFAULT_WEBSITE_ID, + 'price' => null + ]; + + /** + * Option link mapping. + * + * @var array + */ + protected $optionLinkMapping = [ + 'sortorder' => 'sort_order', + 'downloads' => 'number_of_downloads', + 'shareable' => 'is_shareable', + 'url' => 'link_url', + 'file' => 'link_file', + ]; + + /** + * Option sample mapping. + * + * @var array + */ + protected $optionSampleMapping = [ + 'sortorder' => 'sort_order', + 'url' => 'sample_url', + 'file' => 'sample_file', + ]; + + /** + * Num row parsing file + */ + protected $rowNum; + + /** + * @var \Magento\DownloadableImportExport\Helper\Uploader + */ + protected $uploaderHelper; + + /** + * @var \Magento\DownloadableImportExport\Helper\Data + */ + protected $downloadableHelper; + + /** + * Constructor + * + * @param \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory $attrSetColFac + * @param \Magento\Catalog\Model\Resource\Product\Attribute\CollectionFactory $prodAttrColFac + * @param \Magento\Framework\App\Resource $resource + * @param array $params + * @param \Magento\DownloadableImportExport\Helper\Uploader $uploaderHelper + * @param \Magento\DownloadableImportExport\Helper\Data $downloadableHelper + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function __construct( + \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory $attrSetColFac, + \Magento\Catalog\Model\Resource\Product\Attribute\CollectionFactory $prodAttrColFac, + \Magento\Framework\App\Resource $resource, + array $params, + \Magento\DownloadableImportExport\Helper\Uploader $uploaderHelper, + \Magento\DownloadableImportExport\Helper\Data $downloadableHelper + ) { + parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params); + $this->parameters = $this->_entityModel->getParameters(); + $this->_resource = $resource; + $this->connection = $resource->getConnection('write'); + $this->uploaderHelper = $uploaderHelper; + $this->downloadableHelper = $downloadableHelper; + } + + /** + * Save product type specific data. + * + * @return \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType + */ + public function saveData() + { + $newSku = $this->_entityModel->getNewSku(); + while ($bunch = $this->_entityModel->getNextBunch()) { + foreach ($bunch as $rowNum => $rowData) { + if (!$this->_entityModel->isRowAllowedToImport($rowData, $rowNum)) { + continue; + } + $productData = $newSku[$rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_SKU]]; + if ($this->_type != $productData['type_id']) { + continue; + } + $this->parseOptions($rowData, $productData['entity_id']); + } + if (!empty($this->cachedOptions['sample']) || !empty($this->cachedOptions['link'])) { + $this->saveOptions(); + $this->clear(); + } + } + return $this; + } + + /** + * Validate row attributes. Pass VALID row data ONLY as argument. + * + * @param array $rowData + * @param int $rowNum + * @param bool $isNewProduct Optional + * + * @return bool + */ + public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) + { + $this->rowNum = $rowNum; + $error = false; + if (!$this->downloadableHelper->isRowDownloadableNoValid($rowData)) { + $this->_entityModel->addRowError(self::ERROR_OPTIONS_NOT_FOUND, $this->rowNum); + $error = true; + } + if ($this->downloadableHelper->isRowDownloadableEmptyOptions($rowData)) { + $this->_entityModel->addRowError(self::ERROR_COLS_IS_EMPTY, $this->rowNum); + $error = true; + } + if ($this->isRowValidSample($rowData) || $this->isRowValidLink($rowData)) { + $error = true; + } + return !$error; + } + + /** + * Validation sample options + * + * @param array $rowData + * @return bool + */ + protected function isRowValidSample(array $rowData) + { + $result = false; + if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) + && $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '' + && $this->sampleGroupTitle($rowData) == '') { + $this->_entityModel->addRowError(self::ERROR_GROUP_TITLE_NOT_FOUND, $this->rowNum); + $result = true; + } + if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) + && $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '') { + $result = $this->isTitle($this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES])); + } + return $result; + } + + /** + * Validation links option + * + * @param array $rowData + * @return bool + */ + protected function isRowValidLink(array $rowData) + { + $result = false; + if (isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && + $rowData[self::COL_DOWNLOADABLE_LINKS] != '' && + $this->linksAdditionalAttributes($rowData, 'group_title', self::DEFAULT_GROUP_TITLE) == '' + ) { + $this->_entityModel->addRowError(self::ERROR_GROUP_TITLE_NOT_FOUND, $this->rowNum); + $result = true; + } + if (isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && + $rowData[self::COL_DOWNLOADABLE_LINKS] != '' + ) { + $result = $this->isTitle($this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS])); + } + return $result; + } + + /** + * Check isset title for all options + * + * @param array $options + * @return bool + */ + protected function isTitle(array $options) + { + $result = false; + foreach ($options as $option) { + if (!array_key_exists('title', $option)) { + $this->_entityModel->addRowError(self::ERROR_OPTION_NO_TITLE, $this->rowNum); + $result = true; + } + } + return $result; + } + + /** + * Prepare attributes with default value for save. + * + * @param array $rowData + * @param bool $withDefaultValue + * @return array + */ + public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDefaultValue = true) + { + $resultAttrs = parent::prepareAttributesWithDefaultValueForSave($rowData, $withDefaultValue); + $resultAttrs = array_merge($resultAttrs, $this->addAdditionalAttributes($rowData)); + return $resultAttrs; + } + + /** + * Get additional attributes for dowloadable product. + * + * @param array $rowData + * @return array + */ + protected function addAdditionalAttributes(array $rowData) + { + return [ + 'samples_title' => $this->sampleGroupTitle($rowData), + 'links_title' => $this->linksAdditionalAttributes($rowData, 'group_title', self::DEFAULT_GROUP_TITLE), + 'links_purchased_separately' => $this->linksAdditionalAttributes( + $rowData, + 'purchased_separately', + self::DEFAULT_PURCHASED_SEPARATELY + ) + ]; + } + + /** + * Get additional attributes for links + * + * @param array $rowData + * @param string $attribute + * @param mixed $defaultValue + * @return string + */ + protected function linksAdditionalAttributes(array $rowData, $attribute, $defaultValue) + { + $result = $defaultValue; + if (isset($rowData[self::COL_DOWNLOADABLE_LINKS])) { + $options = explode( + \Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, + $rowData[self::COL_DOWNLOADABLE_LINKS] + ); + foreach ($options as $option) { + $arr = $this->parseLinkOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + if (isset($arr[$attribute])) { + $result = $arr[$attribute]; + break; + } + } + } + return $result; + } + + /** + * Get group title for sample + * + * @param array $rowData + * @return string + */ + protected function sampleGroupTitle(array $rowData) + { + $result = ''; + if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { + $options = explode( + \Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, + $rowData[self::COL_DOWNLOADABLE_SAMPLES] + ); + foreach ($options as $option) { + $arr = $this->parseSampleOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + if (isset($arr['group_title'])) { + $result = $arr['group_title']; + break; + } + } + } + return $result; + } + + /** + * Parse options for products + * + * @param array $rowData + * @param int $entityId + * @return $this + */ + protected function parseOptions(array $rowData, $entityId) + { + $this->productIds[] = $entityId; + if (isset($rowData[self::COL_DOWNLOADABLE_LINKS])) { + $this->cachedOptions['link'] = array_merge( + $this->cachedOptions['link'], + $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS], $entityId) + ); + } + if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { + $this->cachedOptions['sample'] = array_merge( + $this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES], $entityId), + $this->cachedOptions['sample'] + ); + } + return $this; + } + + /** + * Get fill data options with key sample + * + * @param array $base + * @param array $options + * @return array + */ + protected function fillDataSample(array $base, array $options) + { + $result = []; + $existingOptions = $this->connection->fetchAll( + $this->connection->select()->from( + $this->_resource->getTableName('downloadable_sample') + )->where( + 'product_id in (?)', + $this->productIds + ) + ); + foreach ($options as $option) { + $isExist = false; + foreach ($existingOptions as $existingOption) { + if ($option['sample_url'] == $existingOption['sample_url'] + && $option['sample_file'] == $existingOption['sample_file'] + && $option['sample_type'] == $existingOption['sample_type'] + && $option['product_id'] == $existingOption['product_id']) { + $result[] = array_replace($this->dataSampleTitle, $option, $existingOption); + $isExist = true; + } + } + if (!$isExist) { + $result[] = array_replace($base, $option); + } + } + return $result; + } + + /** + * Get fill data options with key link + * + * @param array $base + * @param array $options + * @return array + */ + protected function fillDataLink(array $base, array $options) + { + $result = []; + $existingOptions = $this->connection->fetchAll( + $this->connection->select()->from( + $this->_resource->getTableName('downloadable_link') + )->where( + 'product_id in (?)', + $this->productIds + ) + ); + foreach ($options as $option) { + $existOption = $this->downloadableHelper->fillExistOptions($base, $option, $existingOptions); + if (empty($existOption)) { + $result[] = array_replace($base, $option); + } else { + $result[] = $existOption; + } + } + return $result; + } + + /** + * Get fill data options with key link + * + * @param array $options + * @return array + */ + protected function fillDataTitleLink(array $options) + { + $result = []; + $existingOptions = $this->connection->fetchAll( + $this->connection->select()->from( + ['dl' => $this->_resource->getTableName('downloadable_link')], + [ + 'link_id', + 'product_id', + 'sort_order', + 'number_of_downloads', + 'is_shareable', + 'link_url', + 'link_file', + 'link_type', + 'sample_url', + 'sample_file', + 'sample_type' + ] + )->joinLeft( + ['dlp' => $this->_resource->getTableName('downloadable_link_price')], + 'dl.link_id = dlp.link_id AND dlp.website_id=' . self::DEFAULT_WEBSITE_ID, + ['price_id', 'website_id'] + )->where( + 'product_id in (?)', + $this->productIds + ) + ); + foreach ($options as $option) { + $existOption = $this->downloadableHelper->fillExistOptions($this->dataLinkTitle, $option, $existingOptions); + if (!empty($existOption)) { + $result['title'][] = $existOption; + } + $existOption = $this->downloadableHelper->fillExistOptions($this->dataLinkPrice, $option, $existingOptions); + if (!empty($existOption)) { + $result['price'][] = $existOption; + } + } + return $result; + } + + /** + * Upload all sample files + * + * @param array $options + * @return array + */ + protected function uploadSampleFiles(array $options) + { + $result = []; + foreach ($options as $option) { + if ($option['sample_file'] !== null) { + $option['sample_file'] = $this->uploadDownloadableFiles($option['sample_file'], 'samples', true); + } + $result[] = $option; + } + return $result; + } + + /** + * Upload all link files + * + * @param array $options + * @return array + */ + protected function uploadLinkFiles(array $options) + { + $result = []; + foreach ($options as $option) { + if ($option['sample_file'] !== null) { + $option['sample_file'] = $this->uploadDownloadableFiles($option['sample_file'], 'link_samples', true); + } + if ($option['link_file'] !== null) { + $option['link_file'] = $this->uploadDownloadableFiles($option['link_file'], 'links', true); + } + $result[] = $option; + } + return $result; + } + + /** + * Save options in base + * + * @return \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType + */ + protected function saveOptions() + { + $options = $this->cachedOptions; + if (!empty($options['sample'])) { + $this->saveSampleOptions(); + } + if (!empty($options['link'])) { + $this->saveLinkOptions(); + } + return $this; + } + + /** + * Save sample options + * + * @return $this + */ + protected function saveSampleOptions() + { + $options['sample'] = $this->uploadSampleFiles($this->cachedOptions['sample']); + $dataSample = $this->fillDataSample($this->dataSample, $options['sample']); + $this->connection->insertOnDuplicate( + $this->_resource->getTableName('downloadable_sample'), + $this->downloadableHelper->prepareDataForSave($this->dataSample, $dataSample) + ); + $titleSample = $this->fillDataSample($this->dataSampleTitle, $options['sample']); + $this->connection->insertOnDuplicate( + $this->_resource->getTableName('downloadable_sample_title'), + $this->downloadableHelper->prepareDataForSave($this->dataSampleTitle, $titleSample) + ); + return $this; + } + + /** + * Save link options + * + * @return $this + */ + protected function saveLinkOptions() + { + $options['link'] = $this->uploadLinkFiles($this->cachedOptions['link']); + $dataLink = $this->fillDataLink($this->dataLink, $options['link']); + $this->connection->insertOnDuplicate( + $this->_resource->getTableName('downloadable_link'), + $this->downloadableHelper->prepareDataForSave($this->dataLink, $dataLink) + ); + $dataLink = $this->fillDataTitleLink($options['link']); + $this->connection->insertOnDuplicate( + $this->_resource->getTableName('downloadable_link_title'), + $this->downloadableHelper->prepareDataForSave($this->dataLinkTitle, $dataLink['title']) + ); + if (count($dataLink['price'])) { + $this->connection->insertOnDuplicate( + $this->_resource->getTableName('downloadable_link_price'), + $this->downloadableHelper->prepareDataForSave($this->dataLinkPrice, $dataLink['price']) + ); + } + return $this; + } + + /** + * Prepare string to array data sample + * + * @param string $rowCol + * @param int $entityId + * @return array + */ + protected function prepareSampleData($rowCol, $entityId = null) + { + $result = []; + $options = explode( + \Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, + $rowCol + ); + foreach ($options as $option) { + $result[] = array_merge( + $this->dataSample, + ['product_id' => $entityId], + $this->parseSampleOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)) + ); + } + return $result; + } + + /** + * Prepare string to array data link + * + * @param string $rowCol + * @param string $entityId + * @return array + */ + protected function prepareLinkData($rowCol, $entityId = null) + { + $result = []; + $options = explode( + \Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, + $rowCol + ); + foreach ($options as $option) { + $result[] = array_merge( + $this->dataLink, + ['product_id' => $entityId], + $this->parseLinkOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)) + ); + } + return $result; + } + + /** + * Parse the link option. + * + * @param array $values + * @return array + */ + protected function parseLinkOption(array $values) + { + $option = []; + foreach ($values as $keyValue) { + $keyValue = trim($keyValue); + if ($pos = strpos($keyValue, self::PAIR_VALUE_SEPARATOR)) { + $key = substr($keyValue, 0, $pos); + $value = substr($keyValue, $pos + 1); + if ($key == 'sample') { + $option['sample_type'] = $this->downloadableHelper->getTypeByValue($value); + $option['sample_' . $option['sample_type']] = $value; + } + if ($key == self::URL_OPTION_VALUE || $key == self::FILE_OPTION_VALUE) { + $option['link_type'] = $key; + } + if ($key == 'downloads' && $value == 'unlimited') { + $value = 0; + } + if (isset($this->optionLinkMapping[$key])) { + $key = $this->optionLinkMapping[$key]; + } + $option[$key] = $value; + } + } + return $option; + } + + /** + * Parse the sample option. + * + * @param array $values + * @return array + */ + protected function parseSampleOption($values) + { + $option = []; + foreach ($values as $keyValue) { + $keyValue = trim($keyValue); + if ($pos = strpos($keyValue, self::PAIR_VALUE_SEPARATOR)) { + $key = substr($keyValue, 0, $pos); + $value = substr($keyValue, $pos + 1); + if ($key == self::URL_OPTION_VALUE || $key == self::FILE_OPTION_VALUE) { + $option['sample_type'] = $key; + } + if (isset($this->optionSampleMapping[$key])) { + $key = $this->optionSampleMapping[$key]; + } + $option[$key] = $value; + } + } + return $option; + } + + /** + * Uploading files into the "downloadable/files" media folder. + * Return a new file name if the same file is already exists. + * + * @param string $fileName + * @param string $type + * @param bool $renameFileOff + * @return string + */ + protected function uploadDownloadableFiles($fileName, $type = 'links', $renameFileOff = false) + { + try { + $res = $this->uploaderHelper->getUploader($type, $this->parameters)->move($fileName, $renameFileOff); + return $res['file']; + } catch (\Exception $e) { + $this->_entityModel->addRowError(self::ERROR_MOVE_FILE, $this->rowNum); + return ''; + } + } + + /** + * Clear cached values between bunches + * + * @return \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType + */ + protected function clear() + { + $this->cachedOptions = [ + 'link' => [], + 'sample' => [] + ]; + $this->productIds = []; + return $this; + } +} diff --git a/app/code/Magento/DownloadableImportExport/README.md b/app/code/Magento/DownloadableImportExport/README.md new file mode 100644 index 0000000000000..0f9c66837519f --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/README.md @@ -0,0 +1 @@ +The Magento_DownloadableImportExport module handles the import and export of the downloadable products. diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Import/Product/Type/DownloadableTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Import/Product/Type/DownloadableTest.php new file mode 100644 index 0000000000000..93a353f68089b --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Import/Product/Type/DownloadableTest.php @@ -0,0 +1,858 @@ +connectionMock = $this->getMock( + 'Magento\Framework\DB\Adapter\Pdo\Mysql', + ['select', 'fetchAll', 'fetchPairs', 'joinLeft', 'insertOnDuplicate', 'delete', 'quoteInto', 'fetchAssoc'], + [], + '', + false + ); + $this->select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); + $this->select->expects($this->any())->method('from')->will($this->returnSelf()); + $this->select->expects($this->any())->method('where')->will($this->returnSelf()); + $this->select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); + $adapter = $this->getMock('Magento\Framework\DB\Adapter\Pdo\Mysql', [], [], '', false); + $adapter->expects($this->any())->method('quoteInto')->will($this->returnValue('query')); + $this->select->expects($this->any())->method('getAdapter')->willReturn($adapter); + $this->connectionMock->expects($this->any())->method('select')->will($this->returnValue($this->select)); + + $this->connectionMock->expects($this->any())->method('insertOnDuplicate')->willReturnSelf(); + $this->connectionMock->expects($this->any())->method('delete')->willReturnSelf(); + $this->connectionMock->expects($this->any())->method('quoteInto')->willReturn(''); + + //constructor arguments: + // 1. $attrSetColFac + $this->attrSetColFacMock = $this->getMock( + 'Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory', + ['create'], + [], + '', + false + ); + $this->attrSetColMock = $this->getMock( + 'Magento\Eav\Model\Resource\Entity\Attribute\Set\Collection', + ['setEntityTypeFilter'], + [], + '', + false + ); + $this->attrSetColMock + ->expects($this->any()) + ->method('setEntityTypeFilter') + ->will($this->returnValue([])); + + // 2. $prodAttrColFac + $this->prodAttrColFacMock = $this->getMock( + 'Magento\Catalog\Model\Resource\Product\Attribute\CollectionFactory', + ['create'], + [], + '', + false + ); + + $attrCollection = $this->getMock( + '\Magento\Catalog\Model\Resource\Product\Attribute\Collection', + [], + [], + '', + false + ); + + $attrCollection->expects($this->any())->method('addFieldToFilter')->willReturn([]); + $this->prodAttrColFacMock->expects($this->any())->method('create')->will($this->returnValue($attrCollection)); + + // 3. $resource + $this->resourceMock = $this->getMock( + 'Magento\Framework\App\Resource', + ['getConnection', 'getTableName'], + [], + '', + false + ); + $this->resourceMock->expects($this->any())->method('getConnection')->will( + $this->returnValue($this->connectionMock) + ); + $this->resourceMock->expects($this->any())->method('getTableName')->will( + $this->returnValue('tableName') + ); + + // 4. $params + $this->entityModelMock = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Product', + [ + 'addMessageTemplate', + 'getEntityTypeId', + 'getBehavior', + 'getNewSku', + 'getNextBunch', + 'isRowAllowedToImport', + 'getParameters', + 'addRowError' + ], + [], + '', + false + ); + + $this->entityModelMock->expects($this->any())->method('addMessageTemplate')->will($this->returnSelf()); + $this->entityModelMock->expects($this->any())->method('getEntityTypeId')->will($this->returnValue(5)); + $this->entityModelMock->expects($this->any())->method('getParameters')->will($this->returnValue([])); + $this->paramsArray = [ + $this->entityModelMock, + 'downloadable' + ]; + + $this->uploaderMock = $this->getMock( + '\Magento\CatalogImportExport\Model\Import\Uploader', + ['move'], + [], + '', + false + ); + + // 6. $filesystem + $this->directoryWriteMock = $this->getMock('Magento\Framework\Filesystem\Directory\Write', [], [], '', false); + + // 7. $fileHelper + $this->uploaderHelper = $this->getMock( + '\Magento\DownloadableImportExport\Helper\Uploader', + ['getUploader'], + [], + '', + false + ); + $this->uploaderHelper->expects($this->any())->method('getUploader')->willReturn($this->uploaderMock); + $this->downloadableHelper = $this->getMock( + '\Magento\DownloadableImportExport\Helper\Data', + ['prepareDataForSave'], + [], + '', + false + ); + $this->downloadableHelper->expects($this->any())->method('prepareDataForSave')->willReturn([]); + } + + /** + * @dataProvider dataForSave + */ + public function testSaveDataAppend($newSku, $bunch, $allowImport, $fetchResult) + { + $this->entityModelMock->expects($this->once())->method('getNewSku')->will($this->returnValue($newSku)); + $this->entityModelMock->expects($this->at(1))->method('getNextBunch')->will($this->returnValue($bunch)); + $this->entityModelMock->expects($this->at(2))->method('getNextBunch')->will($this->returnValue(null)); + $this->entityModelMock->expects($this->any())->method('isRowAllowedToImport')->willReturn($allowImport); + + $this->uploaderMock->expects($this->any())->method('setTmpDir')->willReturn(true); + $this->uploaderMock->expects($this->any())->method('setDestDir')->with('pub/media/')->willReturn(true); + + $this->connectionMock->expects($this->any())->method('fetchAll')->with( + $this->select + )->will($this->onConsecutiveCalls( + [ + [ + 'attribute_set_name' => '1', + 'attribute_id' => '1', + ], + [ + 'attribute_set_name' => '2', + 'attribute_id' => '2', + ], + ], + $fetchResult['sample'], + $fetchResult['sample'], + $fetchResult['link'], + $fetchResult['link'] + )); + + $downloadableModelMock = $this->objectManagerHelper->getObject( + '\Magento\DownloadableImportExport\Model\Import\Product\Type\Downloadable', + [ + 'attrSetColFac' => $this->attrSetColFacMock, + 'prodAttrColFac' => $this->prodAttrColFacMock, + 'resource' => $this->resourceMock, + 'params' => $this->paramsArray, + 'uploaderHelper' => $this->uploaderHelper, + 'downloadableHelper' => $this->downloadableHelper + ] + ); + + $downloadableModelMock->saveData(); + } + + /** + * Data for method testSaveDataAppend + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function dataForSave() + { + return [ + [ + 'newSku' => [ + 'downloadablesku1' => [ + 'entity_id' => '25', + 'type_id' => 'downloadable', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku1', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4' + .',sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title,' + .'title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + ], + 'allowImport' => true, + [ + 'sample' => [ + [ + 'sample_id' => '65', + 'product_id' => '25', + 'sample_url' => null, + 'sample_file' => '', + 'sample_type' => 'file', + 'sort_order' => '1', + ], + [ + 'sample_id' => '66', + 'product_id' => '25', + 'sample_url' => 'media/file2.mp4', + 'sample_file' => null, + 'sample_type' => 'url', + 'sort_order' => '0', + ] + ], + 'link' => [ + [ + 'link_id' => '65', + 'product_id' => '25', + 'sort_order' => '1', + 'number_of_downloads' => '0', + 'is_shareable' => '2', + 'link_url' => null, + 'link_file' => '', + 'link_type' => 'file', + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null, + ], + [ + 'link_id' => '66', + 'product_id' => '25', + 'sort_order' => '0', + 'number_of_downloads' => '0', + 'is_shareable' => '2', + 'link_url' => 'media/file2.mp4', + 'link_file' => null, + 'link_type' => 'url', + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null, + ] + ] + ] + ], + [ + 'newSku' => [ + 'downloadablesku2' => [ + 'entity_id' => '25', + 'type_id' => 'downloadable', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku2', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4' + .',sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title, ' + .'title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + ], + 'allowImport' => false, + ['sample' => [], 'link' => []] + ], + [ + 'newSku' => [ + 'downloadablesku3' => [ + 'entity_id' => '25', + 'type_id' => 'simple', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku3', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title,' + .' title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + ], + 'allowImport' => true, + ['sample' => [], 'link' => []] + ], + [ + 'newSku' => [ + 'downloadablesku4' => [ + 'entity_id' => '25', + 'type_id' => 'downloadable', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku4', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title,' + .' title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + ], + 'allowImport' => true, + [ + 'sample' => [ + [ + 'sample_id' => '65', + 'product_id' => '25', + 'sample_url' => null, + 'sample_file' => '', + 'sample_type' => 'file', + 'sort_order' => '1', + ], + [ + 'sample_id' => '66', + 'product_id' => '25', + 'sample_url' => 'media/some_another_file.mp4', + 'sample_file' => null, + 'sample_type' => 'url', + 'sort_order' => '0', + ] + ], + 'link' => [ + [ + 'link_id' => '65', + 'product_id' => '25', + 'sort_order' => '1', + 'number_of_downloads' => '0', + 'is_shareable' => '2', + 'link_url' => null, + 'link_file' => '', + 'link_type' => 'file', + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null, + ], + [ + 'link_id' => '66', + 'product_id' => '25', + 'sort_order' => '0', + 'number_of_downloads' => '0', + 'is_shareable' => '2', + 'link_url' => 'media/some_another_file.mp4', + 'link_file' => null, + 'link_type' => 'url', + 'sample_url' => null, + 'sample_file' => null, + 'sample_type' => null, + ] + ] + ] + ], + [ + 'newSku' => [ + 'downloadablesku5' => [ + 'entity_id' => '25', + 'type_id' => 'downloadable', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku5', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title, title=Title 2, price=10, downloads=unlimited,' + .' url=http://www.sample.com/pic.jpg,sortorder=0,sample=http://www.sample.com/pic.jpg,' + .'purchased_separately=1,shareable=1|group_title=Group Title, title=Title 2, price=10, ' + .'downloads=unlimited, url=media/file2.mp4,sortorder=0,sample=media/file2mp4', + ], + ], + 'allowImport' => true, + [ + 'sample' => [ + [ + 'sample_id' => '65', + 'product_id' => '25', + 'sample_url' => null, + 'sample_file' => '', + 'sample_type' => 'file', + 'sort_order' => '1', + ], + [ + 'sample_id' => '66', + 'product_id' => '25', + 'sample_url' => 'media/file2.mp4', + 'sample_file' => null, + 'sample_type' => 'url', + 'sort_order' => '0', + ] + ], + 'link' => [ + [ + 'link_id' => '65', + 'product_id' => '25', + 'sort_order' => '1', + 'number_of_downloads' => '0', + 'is_shareable' => '1', + 'link_url' => 'http://www.sample.com/pic.jpg', + 'link_file' => null, + 'link_type' => 'url', + 'sample_url' => 'http://www.sample.com/pic.jpg', + 'sample_file' => null, + 'sample_type' => 'url', + ], + [ + 'link_id' => '66', + 'product_id' => '25', + 'sort_order' => '0', + 'number_of_downloads' => '0', + 'is_shareable' => '2', + 'link_url' => 'media/file2.mp4', + 'link_file' => null, + 'link_type' => 'url', + 'sample_url' => null, + 'sample_file' => 'f/i/file.png', + 'sample_type' => 'file', + ] + ] + ] + ], + ]; + } + + /** + * @dataProvider isRowValidData + */ + public function testIsRowValid(array $rowData, $rowNum, $isNewProduct = true) + { + $this->connectionMock->expects($this->any())->method('fetchAll')->with( + $this->select + )->willReturnOnConsecutiveCalls( + [ + [ + 'attribute_set_name' => '1', + 'attribute_id' => '1', + ], + [ + 'attribute_set_name' => '2', + 'attribute_id' => '2', + ], + ] + ); + $this->downloadableModelMock = $this->objectManagerHelper->getObject( + '\Magento\DownloadableImportExport\Model\Import\Product\Type\Downloadable', + [ + 'attrSetColFac' => $this->attrSetColFacMock, + 'prodAttrColFac' => $this->prodAttrColFacMock, + 'resource' => $this->resourceMock, + 'params' => $this->paramsArray, + 'uploaderHelper' => $this->uploaderHelper, + 'downloadableHelper' => $this->downloadableHelper + ] + ); + $this->downloadableModelMock->isRowValid($rowData, $rowNum, $isNewProduct); + } + + /** + * Data for method testIsRowValid + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function isRowValidData() + { + return [ + [ + [ + 'sku' => 'downloadablesku1', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10, ' + .'downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title, ' + .'title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + 0, + true + ], + [ + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4' + .',sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file.mp4,sortorder=1|group_title=Group Title, title=Title 2,' + .' price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + 1, + true + ], + [ + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + ], + 2, + true + ], + [ + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => 'title=Title 1, file=media/file.mp4,sortorder=1|title=Title 2,' + .' url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'title=Title 1, price=10, downloads=unlimited, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, price=10, downloads=unlimited,' + .' url=media/file2.mp4,sortorder=0', + ], + 3, + true + ], + [ + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => 'file=media/file.mp4,sortorder=1|group_title=Group Title, ' + .'url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'title=Title 1, price=10, downloads=unlimited, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, price=10, downloads=unlimited,' + .' url=media/file2.mp4,sortorder=0', + ], + 4, + true + ], + [ //empty group title samples + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => 'group_title=, title=Title 1, file=media/file.mp4,sortorder=1' + .'|group_title=, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10,' + .' downloads=unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title,' + .' title=Title 2, price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + 5, + true + ], + [ //empty group title links + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,' + .'sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=, title=Title 1, price=10, downloads=unlimited, ' + .'file=media/file_link.mp4,sortorder=1|group_title=, title=Title 2, price=10, ' + .'downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + 6, + true + ], + [ + [ + 'sku' => 'downloadablesku12', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 2', + 'downloadable_samples' => '', + 'downloadable_links' => '', + ], + 7, + true + ], + ]; + } + + /** + * @dataProvider dataForUploaderDir + */ + public function testSetUploaderDirFalse($newSku, $bunch, $allowImport) + { + $this->connectionMock->expects($this->any())->method('fetchAll')->with( + $this->select + )->willReturnOnConsecutiveCalls( + [ + [ + 'attribute_set_name' => '1', + 'attribute_id' => '1', + ], + [ + 'attribute_set_name' => '2', + 'attribute_id' => '2', + ], + ] + ); + $this->downloadableModelMock = $this->objectManagerHelper->getObject( + '\Magento\DownloadableImportExport\Model\Import\Product\Type\Downloadable', + [ + 'attrSetColFac' => $this->attrSetColFacMock, + 'prodAttrColFac' => $this->prodAttrColFacMock, + 'resource' => $this->resourceMock, + 'params' => $this->paramsArray, + 'uploaderHelper' => $this->uploaderHelper, + 'downloadableHelper' => $this->downloadableHelper + ] + ); + $this->entityModelMock->expects($this->once())->method('getNewSku')->will($this->returnValue($newSku)); + $this->entityModelMock->expects($this->at(1))->method('getNextBunch')->will($this->returnValue($bunch)); + $this->entityModelMock->expects($this->at(2))->method('getNextBunch')->will($this->returnValue(null)); + $this->entityModelMock->expects($this->any())->method('isRowAllowedToImport')->willReturn($allowImport); + $exception = new \Magento\Framework\Exception\LocalizedException(new \Magento\Framework\Phrase('Error')); + $this->setExpectedException('\Magento\Framework\Exception\LocalizedException'); + $this->setExpectedException('\Exception'); + $this->uploaderMock->expects($this->any())->method('move')->will($this->throwException($exception)); + $this->downloadableModelMock->saveData(); + } + + /** + * Data for methods testSetUploaderDirFalse, testSetDestDirFalse, testDirWithoutPermissions + * + * @return array + */ + public function dataForUploaderDir() + { + return [ + [ + 'newSku' => [ + 'downloadablesku1' => [ + 'entity_id' => '25', + 'type_id' => 'downloadable', + 'attr_set_id' => '4', + 'attr_set_code' => 'Default', + ], + ], + 'bunch' => [ + [ + 'sku' => 'downloadablesku1', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4' + .',sortorder=1|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10, downloads=' + .'unlimited, file=media/file_link.mp4,sortorder=1|group_title=Group Title, title=Title 2,' + .' price=10, downloads=unlimited, url=media/file2.mp4,sortorder=0', + ], + ], + 'allowImport' => true, + ], + ]; + } + + /** + * Test for method prepareAttributesWithDefaultValueForSave + */ + public function testPrepareAttributesWithDefaultValueForSave() + { + $rowData = [ + '_attribute_set' => 'Default', + 'sku' => 'downloadablesku1', + 'product_type' => 'downloadable', + 'name' => 'Downloadable Product 1', + 'downloadable_samples' => 'group_title=Group Title Samples, title=Title 1, file=media/file.mp4,sortorder=1' + .'|group_title=Group Title, title=Title 2, url=media/file2.mp4,sortorder=0', + 'downloadable_links' => 'group_title=Group Title Links, title=Title 1, price=10, downloads=unlimited,' + .' file=media/file_link.mp4,sortorder=1|group_title=Group Title, title=Title 2, price=10, downloads' + .'=unlimited, url=media/file2.mp4,sortorder=0', + ]; + $this->connectionMock->expects($this->any())->method('fetchAll')->with( + $this->select + )->willReturnOnConsecutiveCalls( + [ + [ + 'attribute_set_name' => '1', + 'attribute_id' => '1', + ], + [ + 'attribute_set_name' => '2', + 'attribute_id' => '2', + ], + ] + ); + $this->downloadableModelMock = $this->objectManagerHelper->getObject( + '\Magento\DownloadableImportExport\Model\Import\Product\Type\Downloadable', + [ + 'attrSetColFac' => $this->attrSetColFacMock, + 'prodAttrColFac' => $this->prodAttrColFacMock, + 'resource' => $this->resourceMock, + 'params' => $this->paramsArray, + 'uploaderHelper' => $this->uploaderHelper, + 'downloadableHelper' => $this->downloadableHelper + ] + ); + $this->setPropertyValue( + $this->downloadableModelMock, + '_attributes', + [ + 'Default' => [ + 'name' => [ + 'id' => '69', + 'code' => 'name', + 'is_global' => '0', + 'is_required' => '1', + 'is_unique' => '0', + 'frontend_label' => 'Name', + 'is_static' => false, + 'apply_to' => + [ + ], + 'type' => 'varchar', + 'default_value' => null, + 'options' => + [ + ], + ], + 'sku' => [ + 'id' => '70', + 'code' => 'sku', + 'is_global' => '1', + 'is_required' => '1', + 'is_unique' => '1', + 'frontend_label' => 'SKU', + 'is_static' => true, + 'apply_to' => + [ + ], + 'type' => 'varchar', + 'default_value' => null, + 'options' => + [ + ], + ] + ] + ] + ); + + $this->downloadableModelMock->prepareAttributesWithDefaultValueForSave($rowData); + } + + /** + * @param $object + * @param $property + * @param $value + */ + protected function setPropertyValue(&$object, $property, $value) + { + $reflection = new \ReflectionClass(get_class($object)); + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + return $object; + } +} diff --git a/app/code/Magento/DownloadableImportExport/composer.json b/app/code/Magento/DownloadableImportExport/composer.json new file mode 100644 index 0000000000000..6d79b90effd59 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/composer.json @@ -0,0 +1,29 @@ +{ + "name": "magento/module-downloadable-import-export", + "description": "N/A", + "require": { + "php": "~5.5.0|~5.6.0", + "magento/module-catalog": "1.0.0-beta", + "magento/module-import-export": "1.0.0-beta", + "magento/module-catalog-import-export": "1.0.0-beta", + "magento/module-downloadable": "1.0.0-beta", + "magento/module-store": "1.0.0-beta", + "magento/module-eav": "1.0.0-beta", + "magento/framework": "1.0.0-beta", + "magento/magento-composer-installer": "*" + }, + "type": "magento2-module", + "version": "1.0.0-beta", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "extra": { + "map": [ + [ + "*", + "Magento/DownloadableImportExport" + ] + ] + } +} diff --git a/app/code/Magento/DownloadableImportExport/etc/import.xml b/app/code/Magento/DownloadableImportExport/etc/import.xml new file mode 100644 index 0000000000000..fcce279dd795d --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/etc/import.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/code/Magento/DownloadableImportExport/etc/module.xml b/app/code/Magento/DownloadableImportExport/etc/module.xml new file mode 100644 index 0000000000000..83e3f829921dc --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Magento/DownloadableImportExport/i18n/en_US.csv b/app/code/Magento/DownloadableImportExport/i18n/en_US.csv new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/GroupedImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/GroupedImportExport/Model/Export/RowCustomizer.php index 0b565e2ac32c8..d47b50b8d1145 100644 --- a/app/code/Magento/GroupedImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/GroupedImportExport/Model/Export/RowCustomizer.php @@ -10,7 +10,12 @@ class RowCustomizer implements RowCustomizerInterface { /** - * @inheritdoc + * Prepare data for export + * + * @param mixed $collection + * @param int $productIds + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function prepareData($collection, $productIds) { @@ -18,23 +23,29 @@ public function prepareData($collection, $productIds) } /** - * @inheritdoc + * Set headers columns + * + * @param array $columns + * @return mixed */ public function addHeaderColumns($columns) { $columns = array_merge( $columns, [ - '_associated_sku', - '_associated_default_qty', - '_associated_position' + 'associated_skus' ] ); return $columns; } /** - * @inheritdoc + * Add data for export + * + * @param array $dataRow + * @param int $productId + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function addData($dataRow, $productId) { @@ -42,7 +53,12 @@ public function addData($dataRow, $productId) } /** - * @inheritdoc + * Calculate the largest links block + * + * @param array $additionalRowsCount + * @param int $productId + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getAdditionalRowsCount($additionalRowsCount, $productId) { diff --git a/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php index 5dc23fbd891a8..3df29a1d05596 100644 --- a/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php @@ -69,6 +69,9 @@ public function saveData() 'relation' => [] ]; foreach ($bunch as $rowNum => $rowData) { + if ($this->_type != $rowData[Product::COL_TYPE]) { + continue; + } $associatedSkusQty = isset($rowData['associated_skus']) ? $rowData['associated_skus'] : null; if (!$this->_entityModel->isRowAllowedToImport($rowData, $rowNum) || empty($associatedSkusQty)) { continue; @@ -96,9 +99,6 @@ public function saveData() } $productId = $productData['entity_id']; - if ($this->_type != $rowData[Product::COL_TYPE]) { - continue; - } $linksData['product_ids'][$productId] = true; $linksData['relation'][] = ['parent_id' => $productId, 'child_id' => $linkedProductId]; $qty = empty($associatedSkuAndQty[1]) ? 0 : trim($associatedSkuAndQty[1]); diff --git a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php new file mode 100644 index 0000000000000..c6d3d9e9d3dc8 --- /dev/null +++ b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -0,0 +1,48 @@ +objectManagerHelper = new ObjectManagerHelper($this); + $this->rowCustomizerMock = $this->objectManagerHelper->getObject( + '\Magento\GroupedImportExport\Model\Export\RowCustomizer' + ); + } + + /** + * Test addHeaderColumns() + */ + public function testAddHeaderColumns() + { + $productData = [0 => 'sku']; + $expectedData = [ + 0 => 'sku', + 1 => 'associated_skus' + ]; + $this->assertEquals($expectedData, $this->rowCustomizerMock->addHeaderColumns($productData)); + } +} diff --git a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php index a2ea856ac2e2c..d6a6b3c40bb21 100644 --- a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php +++ b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php @@ -6,17 +6,13 @@ namespace Magento\GroupedImportExport\Test\Unit\Model\Import\Product\Type; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use \Magento\GroupedImportExport; -class GroupedTest extends \PHPUnit_Framework_TestCase +class GroupedTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** @var GroupedImportExport\Model\Import\Product\Type\Grouped */ protected $grouped; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - /** * @var \Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ @@ -64,6 +60,8 @@ class GroupedTest extends \PHPUnit_Framework_TestCase protected function setUp() { + parent::setUp(); + $this->setCollectionFactory = $this->getMock( 'Magento\Eav\Model\Resource\Entity\Attribute\Set\CollectionFactory', ['create'], @@ -93,11 +91,12 @@ protected function setUp() $this->attrCollectionFactory->expects($this->any())->method('addFieldToFilter')->willReturn([]); $this->entityModel = $this->getMock( 'Magento\CatalogImportExport\Model\Import\Product', - ['getNewSku', 'getOldSku', 'getNextBunch', 'isRowAllowedToImport', 'getRowScope'], + ['getErrorAggregator', 'getNewSku', 'getOldSku', 'getNextBunch', 'isRowAllowedToImport', 'getRowScope'], [], '', false ); + $this->entityModel->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $this->params = [ 0 => $this->entityModel, 1 => 'grouped' @@ -109,12 +108,10 @@ protected function setUp() '', false ); - $entityAttributes = [ - [ - 'attribute_id' => 'attribute_id', - 'attribute_set_name' => 'attribute_set_name' - ] - ]; + $entityAttributes = [[ + 'attribute_set_name' => 'attribute_id', + 'attribute_id' => 'attributeSetName', + ]]; $this->connection = $this->getMock( 'Magento\Framework\DB\Adapter\Pdo\Mysql', ['select', 'fetchAll', 'fetchPairs', 'joinLeft', 'insertOnDuplicate', 'delete', 'quoteInto'], @@ -149,7 +146,6 @@ protected function setUp() ); $this->resource->expects($this->any())->method('getConnection')->will($this->returnValue($this->connection)); $this->resource->expects($this->any())->method('getTableName')->will($this->returnValue('tableName')); - $this->objectManagerHelper = new ObjectManagerHelper($this); $this->grouped = $this->objectManagerHelper->getObject( 'Magento\GroupedImportExport\Model\Import\Product\Type\Grouped', [ diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php index 0057f0a73570b..0010eeba42e29 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php @@ -21,6 +21,8 @@ class Download extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text public function _getValue(\Magento\Framework\DataObject $row) { return '

' . $row->getData('imported_file') . '

Download'; + . $this->getUrl('*/*/download', ['filename' => $row->getData('imported_file')]) . '">' + . __('Download') + . ''; } } diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php new file mode 100644 index 0000000000000..def8e2d833dde --- /dev/null +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php @@ -0,0 +1,32 @@ +getData('error_file') != '') { + $result = '

' . $row->getData('error_file') . '

' + . __('Download') + . ''; + } + return $result; + } +} diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Before.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Before.php index a44a22e86384f..c72ca9d461051 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Before.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Before.php @@ -56,6 +56,20 @@ public function getEntityBehaviors() return $this->_jsonEncoder->encode($behaviors); } + /** + * Returns json-encoded entity behaviors notes array + * + * @return string + */ + public function getEntityBehaviorsNotes() + { + $behaviors = $this->_importModel->getEntityBehaviors(); + foreach ($behaviors as $entityCode => $behavior) { + $behaviors[$entityCode] = $behavior['notes']; + } + return $this->_jsonEncoder->encode($behaviors); + } + /** * Return json-encoded list of existing behaviors * diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index 7ce277171063b..71f342b5b5857 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Block\Adminhtml\Import\Edit; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; + /** * Import edit form block * @@ -108,10 +110,45 @@ protected function _prepareForm() 'disabled' => true, 'values' => $this->_behaviorFactory->create($behaviorClass)->toOptionArray(), 'class' => $behaviorCode, + 'onchange' => 'varienImport.handleImportBehaviorSelector();', + 'note' => ' ', + ] + ); + $fieldsets[$behaviorCode]->addField( + $behaviorCode . \Magento\ImportExport\Model\Import::FIELD_NAME_VALIDATION_STRATEGY, + 'select', + [ + 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_VALIDATION_STRATEGY, + 'title' => __(' '), + 'label' => __(' '), + 'required' => true, + 'class' => $behaviorCode, + 'disabled' => true, + 'values' => [ + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR => 'Stop on Error', + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS => 'Skip error entries' + ], + 'after_element_html' => $this->getDownloadSampleFileHtml(), + ] + ); + $fieldsets[$behaviorCode]->addField( + $behaviorCode . '_' . \Magento\ImportExport\Model\Import::FIELD_NAME_ALLOWED_ERROR_COUNT, + 'text', + [ + 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_ALLOWED_ERROR_COUNT, + 'label' => __('Allowed Errors Count'), + 'title' => __('Allowed Errors Count'), + 'required' => true, + 'disabled' => true, + 'value' => 10, + 'class' => $behaviorCode . ' validate-number validate-greater-than-zero input-text', + 'note' => __( + 'Please specify number of errors to halt import process' + ), ] ); $fieldsets[$behaviorCode]->addField( - $behaviorCode . \Magento\ImportExport\Model\Import::FIELD_FIELD_SEPARATOR, + $behaviorCode . '_' . \Magento\ImportExport\Model\Import::FIELD_FIELD_SEPARATOR, 'text', [ 'name' => \Magento\ImportExport\Model\Import::FIELD_FIELD_SEPARATOR, diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import.php index 319bef1a9cd42..0789bb1d04357 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import.php @@ -6,6 +6,8 @@ namespace Magento\ImportExport\Controller\Adminhtml; use Magento\Backend\App\Action; +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Import controller diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php index f8124344e9dfc..54c9049bcc91d 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php @@ -5,11 +5,34 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; -use Magento\ImportExport\Controller\Adminhtml\Import as ImportController; +use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\Framework\Controller\ResultFactory; -class Start extends ImportController +class Start extends ImportResultController { + /** + * @var \Magento\ImportExport\Model\Import + */ + protected $importModel; + + /** + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor + * @param \Magento\ImportExport\Model\History $historyModel + * @param \Magento\ImportExport\Helper\Report $reportHelper + * @param \Magento\ImportExport\Model\Import $importModel + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor, + \Magento\ImportExport\Model\History $historyModel, + \Magento\ImportExport\Helper\Report $reportHelper, + \Magento\ImportExport\Model\Import $importModel + ) { + parent::__construct($context, $reportProcessor, $historyModel, $reportHelper); + $this->importModel = $importModel; + } + /** * Start import process action * @@ -23,23 +46,26 @@ public function execute() $resultLayout = $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); /** @var $resultBlock \Magento\ImportExport\Block\Adminhtml\Import\Frame\Result */ $resultBlock = $resultLayout->getLayout()->getBlock('import.frame.result'); - /** @var $importModel \Magento\ImportExport\Model\Import */ - $importModel = $this->_objectManager->create('Magento\ImportExport\Model\Import'); + $resultBlock + ->addAction('show', 'import_validation_container') + ->addAction('innerHTML', 'import_validation_container_header', __('Status')) + ->addAction('hide', ['edit_form', 'upload_button', 'messages']); - try { - $importModel->setData($data); - $importModel->importSource(); - $importModel->invalidateIndex(); - $resultBlock->addAction('show', 'import_validation_container') - ->addAction('innerHTML', 'import_validation_container_header', __('Status')); - } catch (\Exception $e) { - $resultBlock->addError($e->getMessage()); - return $resultLayout; + $this->importModel->setData($data); + $this->importModel->importSource(); + $errorAggregator = $this->importModel->getErrorAggregator(); + if ($this->importModel->getErrorAggregator()->hasToBeTerminated()) { + $resultBlock->addError(__('Maximum error count has been reached or system error is occurred!')); + $this->addErrorMessages($resultBlock, $errorAggregator); + } else { + $this->importModel->invalidateIndex(); + $this->addErrorMessages($resultBlock, $errorAggregator); + $resultBlock->addSuccess(__('Import successfully done')); } - $resultBlock->addAction('hide', ['edit_form', 'upload_button', 'messages']) - ->addSuccess(__('Import successfully done')); + return $resultLayout; } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setPath('adminhtml/*/index'); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index e09c555b4cce7..631966a9a40c2 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -5,57 +5,15 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; -use Magento\ImportExport\Controller\Adminhtml\Import as ImportController; +use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Block\Adminhtml\Import\Frame\Result as ImportResultBlock; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; -class Validate extends ImportController +class Validate extends ImportResultController { - /** - * Process validation results - * - * @param \Magento\ImportExport\Model\Import $import - * @param \Magento\ImportExport\Block\Adminhtml\Import\Frame\Result $resultBlock - * @return void - */ - protected function processValidationError( - Import $import, - ImportResultBlock $resultBlock - ) { - if ($import->getProcessedRowsCount() == $import->getInvalidRowsCount()) { - $resultBlock->addNotice(__('This file is invalid. Please fix errors and re-upload the file.')); - } elseif ($import->getErrorsCount() >= $import->getErrorsLimit()) { - $resultBlock->addNotice( - __( - 'You\'ve reached an error limit (%1). Please fix errors and re-upload the file.', - $import->getErrorsLimit() - ) - ); - } else { - if ($import->isImportAllowed()) { - $resultBlock->addNotice( - __( - 'Please fix errors and re-upload the file. Or press "Import" to skip rows with errors.' - ), - true - ); - } else { - $resultBlock->addNotice( - __('The file is partially valid, but we can\'t import it for some reason.'), - false - ); - } - } - // errors info - foreach ($import->getErrors() as $errorCode => $rows) { - $error = $errorCode . ' ' . __('in rows:') . ' ' . implode(', ', $rows); - $resultBlock->addError($error); - } - } - /** * Validate uploaded files action * @@ -66,7 +24,7 @@ public function execute() $data = $this->getRequest()->getPostValue(); /** @var \Magento\Framework\View\Result\Layout $resultLayout */ $resultLayout = $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); - /** @var $resultBlock \Magento\ImportExport\Block\Adminhtml\Import\Frame\Result */ + /** @var $resultBlock ImportResultBlock */ $resultBlock = $resultLayout->getLayout()->getBlock('import.frame.result'); if ($data) { // common actions @@ -75,49 +33,53 @@ public function execute() 'import_validation_container' ); - try { - /** @var $import \Magento\ImportExport\Model\Import */ - $import = $this->_objectManager->create('Magento\ImportExport\Model\Import')->setData($data); - $source = ImportAdapter::findAdapterFor( - $import->uploadSource(), - $this->_objectManager->create('Magento\Framework\Filesystem') - ->getDirectoryWrite(DirectoryList::ROOT), - $data[$import::FIELD_FIELD_SEPARATOR] - ); - $validationResult = $import->validateSource($source); + /** @var $import \Magento\ImportExport\Model\Import */ + $import = $this->_objectManager->create('Magento\ImportExport\Model\Import')->setData($data); + $source = ImportAdapter::findAdapterFor( + $import->uploadSource(), + $this->_objectManager->create('Magento\Framework\Filesystem') + ->getDirectoryWrite(DirectoryList::ROOT), + $data[$import::FIELD_FIELD_SEPARATOR] + ); + $validationResult = $import->validateSource($source); - if (!$import->getProcessedRowsCount()) { + if (!$import->getProcessedRowsCount()) { + if (!$import->getErrorAggregator()->getErrorsCount()) { $resultBlock->addError(__('This file is empty. Please try another one.')); } else { - if (!$validationResult) { - $this->processValidationError($import, $resultBlock); - } else { - if ($import->isImportAllowed()) { - $resultBlock->addSuccess( - __('File is valid! To start import process press "Import" button'), - true - ); - } else { - $resultBlock->addError( - __('The file is valid, but we can\'t import it for some reason.'), - false - ); - } + foreach ($import->getErrorAggregator()->getAllErrors() as $error) { + $resultBlock->addError($error->getErrorMessage(), false); } - $resultBlock->addNotice($import->getNotices()); - $resultBlock->addNotice( - __( - 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', - $import->getProcessedRowsCount(), - $import->getProcessedEntitiesCount(), - $import->getInvalidRowsCount(), - $import->getErrorsCount() - ) + } + } else { + $errorAggregator = $import->getErrorAggregator(); + if (!$validationResult) { + $resultBlock->addError( + __('Data validation is failed. Please fix errors and re-upload the file..') ); + $this->addErrorMessages($resultBlock, $errorAggregator); + } else { + if ($import->isImportAllowed()) { + $resultBlock->addSuccess( + __('File is valid! To start import process press "Import" button'), + true + ); + } else { + $resultBlock->addError( + __('The file is valid, but we can\'t import it for some reason.'), + false + ); + } } - } catch (\Exception $e) { - $resultBlock->addNotice(__('Please fix errors and re-upload the file.')) - ->addError($e->getMessage()); + $resultBlock->addNotice( + __( + 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', + $import->getProcessedRowsCount(), + $import->getProcessedEntitiesCount(), + $errorAggregator->getInvalidRowsCount(), + $errorAggregator->getErrorsCount() + ) + ); } return $resultLayout; } elseif ($this->getRequest()->isPost() && empty($_FILES)) { diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php new file mode 100644 index 0000000000000..bfb3cdb95fd23 --- /dev/null +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php @@ -0,0 +1,151 @@ +reportProcessor = $reportProcessor; + $this->historyModel = $historyModel; + $this->reportHelper = $reportHelper; + } + + /** + * @param \Magento\Framework\View\Element\AbstractBlock $resultBlock + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return $this + */ + protected function addErrorMessages( + \Magento\Framework\View\Element\AbstractBlock $resultBlock, + ProcessingErrorAggregatorInterface $errorAggregator + ) { + if ($errorAggregator->getErrorsCount()) { + $message = ''; + $counter = 0; + foreach ($this->getErrorMessages($errorAggregator) as $error) { + $message .= ++$counter . '. ' . $error . '
'; + if ($counter >= self::LIMIT_ERRORS_MESSAGE) { + break; + } + } + if ($errorAggregator->hasFatalExceptions()) { + foreach ($this->getSystemExceptions($errorAggregator) as $error) { + $message .= $error->getErrorMessage() + . ' ' + . __('Show more') . '
' . __('Additional data') . ': ' + . $error->getErrorDescription() . '
'; + } + } + try { + $resultBlock->addNotice( + '' . __('Following Error(s) has been occurred during importing process:') . '
' + . '
' . __('Only first 100 errors are displayed here. ') + . '' . __('Download full report') . '
' + . '
' . $message . '
' + ); + } catch (\Exception $e) { + foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) { + $resultBlock->addError($errorMessage); + } + } + } + + return $this; + } + + /** + * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface $errorAggregator + * @return array + */ + protected function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator) + { + $messages = []; + $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + foreach ($rowMessages as $errorCode => $rows) { + $messages[] = $errorCode . ' ' . __('in rows:') . ' ' . implode(', ', $rows); + } + return $messages; + } + + /** + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] + */ + protected function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator) + { + return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + } + + /** + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator) + { + $this->historyModel->loadLastInsertItem(); + $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile()); + $writeOnlyErrorItems = true; + if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) { + $writeOnlyErrorItems = false; + } + $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems); + $this->historyModel->addErrorReportFile($fileName); + return $fileName; + } + + /** + * @param string $fileName + * @return string + */ + protected function createDownloadUrlImportHistoryFile($fileName) + { + return $this->getUrl(self::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]); + } +} diff --git a/app/code/Magento/ImportExport/Helper/Report.php b/app/code/Magento/ImportExport/Helper/Report.php index 935a64fe13a5d..f05e0a790f26c 100644 --- a/app/code/Magento/ImportExport/Helper/Report.php +++ b/app/code/Magento/ImportExport/Helper/Report.php @@ -90,6 +90,15 @@ public function getReportOutput($filename) return $this->varDirectory->readFile($this->getFilePath($filename)); } + /** + * @param string $fileName + * @return string + */ + public function getReportAbsolutePath($fileName) + { + return $this->varDirectory->getAbsolutePath(Import::IMPORT_HISTORY_DIR . $fileName); + } + /** * Retrieve report file size * diff --git a/app/code/Magento/ImportExport/Model/Export/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Export/AbstractEntity.php index e11145a53667d..d9780c68c1ab5 100644 --- a/app/code/Magento/ImportExport/Model/Export/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Export/AbstractEntity.php @@ -268,6 +268,20 @@ public function addMessageTemplate($errorCode, $message) return $this; } + /** + * Retrieve message template + * + * @param string $errorCode + * @return null|string + */ + public function retrieveMessageTemplate($errorCode) + { + if (isset($this->_messageTemplates[$errorCode])) { + return $this->_messageTemplates[$errorCode]; + } + return null; + } + /** * Export process * diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/AbstractAdapter.php b/app/code/Magento/ImportExport/Model/Export/Adapter/AbstractAdapter.php index 20a92c0f6c943..4e1087fb6aa9f 100644 --- a/app/code/Magento/ImportExport/Model/Export/Adapter/AbstractAdapter.php +++ b/app/code/Magento/ImportExport/Model/Export/Adapter/AbstractAdapter.php @@ -37,13 +37,17 @@ abstract class AbstractAdapter /** * Constructor * - * @param \Magento\Framework\Filesystem $filesystem + * @param Filesystem $filesystem * @param string|null $destination + * @param string $destinationDirectoryCode * @throws \Magento\Framework\Exception\LocalizedException */ - public function __construct(\Magento\Framework\Filesystem $filesystem, $destination = null) - { - $this->_directoryHandle = $filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + public function __construct( + \Magento\Framework\Filesystem $filesystem, + $destination = null, + $destinationDirectoryCode = DirectoryList::SYS_TMP + ) { + $this->_directoryHandle = $filesystem->getDirectoryWrite($destinationDirectoryCode); if (!$destination) { $destination = uniqid('importexport_'); $this->_directoryHandle->touch($destination); diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php index 136ab28fb7c99..9ff3b8c8b45ec 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php @@ -346,6 +346,20 @@ public function addMessageTemplate($errorCode, $message) return $this; } + /** + * Retrieve message template + * + * @param string $errorCode + * @return null|string + */ + public function retrieveMessageTemplate($errorCode) + { + if (isset($this->_messageTemplates[$errorCode])) { + return $this->_messageTemplates[$errorCode]; + } + return null; + } + /** * Export process. * diff --git a/app/code/Magento/ImportExport/Model/History.php b/app/code/Magento/ImportExport/Model/History.php index 9cdad07dd5d3a..f21cf7edf8eed 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -23,6 +23,8 @@ class History extends \Magento\Framework\Model\AbstractModel const IMPORTED_FILE = 'imported_file'; + const ERROR_FILE = 'error_file'; + const EXECUTION_TIME = 'execution_time'; const SUMMARY = 'summary'; @@ -91,6 +93,19 @@ public function addReport($filename) return $this; } + /** + * Add errors to import history report + * + * @param string $filename + * @return $this + */ + public function addErrorReportFile($filename) + { + $this->setErrorFile($filename); + $this->save(); + return $this; + } + /** * Update import history report * @@ -170,6 +185,16 @@ public function getImportedFile() return $this->getData(self::IMPORTED_FILE); } + /** + * Get error file + * + * @return string + */ + public function getErrorFile() + { + return $this->getData(self::ERROR_FILE); + } + /** * Get import execution time * @@ -234,6 +259,16 @@ public function setImportedFile($importedFile) return $this->setData(self::IMPORTED_FILE, $importedFile); } + /** + * Set error file name + * + * @param string $errorFile + * @return $this + */ + public function setErrorFile($errorFile) + { + return $this->setData(self::ERROR_FILE, $errorFile); + } /** * Set Execution Time * @@ -256,6 +291,16 @@ public function setSummary($summary) return $this->setData(self::SUMMARY, $summary); } + /** + * @return $this + */ + public function loadLastInsertItem() + { + $this->load($this->getLastItemId()); + + return $this; + } + /** * Retrieve admin ID * diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index e46e82e6abe8d..94a7c1a6dd3c1 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -10,6 +10,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Import model @@ -56,6 +58,16 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ const FIELD_NAME_IMG_FILE_DIR = 'import_images_file_dir'; + /** + * Allowed errors count field name + */ + const FIELD_NAME_ALLOWED_ERROR_COUNT = 'allowed_error_count'; + + /** + * Validation startegt field name + */ + const FIELD_NAME_VALIDATION_STRATEGY = 'validation_strategy'; + /** * Import field separator. */ @@ -199,9 +211,9 @@ public function __construct( */ protected function _getEntityAdapter() { + if (!$this->_entityAdapter) { $entities = $this->_importConfig->getEntities(); - if (isset($entities[$this->getEntity()])) { try { $this->_entityAdapter = $this->_entityFactory->create($entities[$this->getEntity()]['model']); @@ -255,31 +267,19 @@ protected function _getSourceAdapter($sourceFile) /** * Return operation result messages * - * @param bool $validationResult + * @param ProcessingErrorAggregatorInterface $validationResult * @return string[] */ - public function getOperationResultMessages($validationResult) + public function getOperationResultMessages(ProcessingErrorAggregatorInterface $validationResult) { $messages = []; if ($this->getProcessedRowsCount()) { - if (!$validationResult) { - if ($this->getProcessedRowsCount() == $this->getInvalidRowsCount()) { - $messages[] = __('This file is invalid. Please fix errors and re-upload the file.'); - } elseif ($this->getErrorsCount() >= $this->getErrorsLimit()) { - $messages[] = __( - 'You\'ve reached an error limit (%1). Please fix errors and re-upload the file.', - $this->getErrorsLimit() - ); - } else { - if ($this->isImportAllowed()) { - $messages[] = __('Please fix errors and re-upload the file.'); - } else { - $messages[] = __('The file is partially valid, but we can\'t import it for some reason.'); - } - } + if ($validationResult->getErrorsCount()) { + $messages[] = __('Data validation is failed. Please fix errors and re-upload the file.'); + // errors info - foreach ($this->getErrors() as $errorCode => $rows) { - $error = $errorCode . ' ' . __('in rows') . ': ' . implode(', ', $rows); + foreach ($validationResult->getRowsGroupedByErrorCode() as $errorMessage => $rows) { + $error = $errorMessage . ' ' . __('in rows') . ': ' . implode(', ', $rows); $messages[] = $error; } } else { @@ -289,16 +289,18 @@ public function getOperationResultMessages($validationResult) $messages[] = __('The file is valid, but we can\'t import it for some reason.'); } } - $notices = $this->getNotices(); - if (is_array($notices)) { - $messages = array_merge($messages, $notices); - } + $messages[] = __( 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', $this->getProcessedRowsCount(), $this->getProcessedEntitiesCount(), - $this->getInvalidRowsCount(), - $this->getErrorsCount() + $validationResult->getInvalidRowsCount(), + $validationResult->getErrorsCount( + [ + ProcessingError::ERROR_LEVEL_CRITICAL, + ProcessingError::ERROR_LEVEL_NOT_CRITICAL + ] + ) ); } else { $messages[] = __('This file does not contain any data.'); @@ -358,56 +360,6 @@ public function getEntity() return $this->_data['entity']; } - /** - * Get entity adapter errors. - * - * @return array - */ - public function getErrors() - { - return $this->_getEntityAdapter()->getErrorMessages(); - } - - /** - * Returns error counter. - * - * @return int - */ - public function getErrorsCount() - { - return $this->_getEntityAdapter()->getErrorsCount(); - } - - /** - * Returns error limit value. - * - * @return int - */ - public function getErrorsLimit() - { - return $this->_getEntityAdapter()->getErrorsLimit(); - } - - /** - * Returns invalid rows count. - * - * @return int - */ - public function getInvalidRowsCount() - { - return $this->_getEntityAdapter()->getInvalidRowsCount(); - } - - /** - * Returns entity model noticees. - * - * @return string[] - */ - public function getNotices() - { - return $this->_getEntityAdapter()->getNotices(); - } - /** * Returns number of checked entities. * @@ -442,7 +394,6 @@ public function getWorkingDir() * Import source file structure to DB. * * @return bool - * @throws \Magento\Framework\Exception\AlreadyExistsException * @throws \Magento\Framework\Exception\LocalizedException */ public function importSource() @@ -453,32 +404,56 @@ public function importSource() $this->addLogComment(__('Begin import of "%1" with "%2" behavior', $this->getEntity(), $this->getBehavior())); - try { - $result = $this->_getEntityAdapter()->importData(); - } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { - $this->importHistoryModel->invalidateReport($this); - throw new \Magento\Framework\Exception\AlreadyExistsException( - __($e->getMessage()) + $result = $this->processImport(); + + if ($result) { + $this->addLogComment( + [ + __( + 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', + $this->getProcessedRowsCount(), + $this->getProcessedEntitiesCount(), + $this->getErrorAggregator()->getInvalidRowsCount(), + $this->getErrorAggregator()->getErrorsCount() + ), + __('The import was successful.'), + ] ); + $this->importHistoryModel->updateReport($this, true); + } else { + $this->importHistoryModel->invalidateReport($this); } - $this->addLogComment( - [ - __( - 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', - $this->getProcessedRowsCount(), - $this->getProcessedEntitiesCount(), - $this->getInvalidRowsCount(), - $this->getErrorsCount() - ), - __('The import was successful.'), - ] - ); - $this->importHistoryModel->updateReport($this, true); return $result; } + /** + * @return bool + */ + protected function processImport() + { + $errorAggregator = $this->_getEntityAdapter()->getErrorAggregator(); + $errorAggregator->initValidationStrategy( + $this->getData(self::FIELD_NAME_VALIDATION_STRATEGY), + $this->getData(self::FIELD_NAME_ALLOWED_ERROR_COUNT) + ); + try { + $this->_getEntityAdapter()->importData(); + } catch (\Exception $e) { + $errorAggregator->addError( + \Magento\ImportExport\Model\Import\Entity\AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION, + ProcessingError::ERROR_LEVEL_CRITICAL, + null, + null, + null, + $e->getMessage() + ); + } + + return !$errorAggregator->hasToBeTerminated(); + } + /** * Import possibility getter. * @@ -489,6 +464,15 @@ public function isImportAllowed() return $this->_getEntityAdapter()->isImportAllowed(); } + /** + * @return ProcessingErrorAggregatorInterface + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getErrorAggregator() + { + return $this->_getEntityAdapter()->getErrorAggregator(); + } + /** * Move uploaded file and create source adapter instance. * @@ -577,11 +561,25 @@ protected function _removeBom($sourceFile) public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $source) { $this->addLogComment(__('Begin data validation')); - $adapter = $this->_getEntityAdapter()->setSource($source); - $result = $adapter->isDataValid(); + try { + $adapter = $this->_getEntityAdapter()->setSource($source); + $errorAggregator = $adapter->validateData(); + } catch (\Exception $e) { + $errorAggregator = $this->getErrorAggregator(); + $errorAggregator->addError( + \Magento\ImportExport\Model\Import\Entity\AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION, + ProcessingError::ERROR_LEVEL_CRITICAL, + null, + null, + null, + $e->getMessage() + ); + } - $messages = $this->getOperationResultMessages($result); + $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); + + $result = !$errorAggregator->getErrorsCount(); if ($result) { $this->addLogComment(__('Import data validation is complete.')); } @@ -636,6 +634,7 @@ public function getEntityBehaviors() $behaviourData[$entityCode] = [ 'token' => $behaviorClassName, 'code' => $behavior->getCode() . '_behavior', + 'notes' => $behavior->getNotes($entityCode), ]; } else { throw new \Magento\Framework\Exception\LocalizedException( @@ -678,12 +677,14 @@ public function getUniqueEntityBehaviors() public function isReportEntityType($entity = null) { $result = false; + if (!$entity) { + $entity = $this->getEntity(); + } if ($entity !== null && $this->_getEntityAdapter()->getEntityTypeCode() != $entity) { $entities = $this->_importConfig->getEntities(); - if (isset($entities[$this->getEntity()])) { + if (isset($entities[$entity])) { try { - $adapter = $this->_entityFactory->create($entities[$entity]['model']); - $result = $adapter->isNeedToLogInHistory(); + $result = $this->_getEntityAdapter()->isNeedToLogInHistory(); } catch (\Exception $e) { throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity model')); } diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index f4a9e039a465f..50dc55f8ce762 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Framework\App\Resource; /** @@ -40,6 +42,38 @@ abstract class AbstractEntity const DB_MAX_TEXT_LENGTH = 65536; + const ERROR_CODE_SYSTEM_EXCEPTION = 'systemException'; + const ERROR_CODE_COLUMN_NOT_FOUND = 'columnNotFound'; + const ERROR_CODE_COLUMN_EMPTY_HEADER = 'columnEmptyHeader'; + const ERROR_CODE_COLUMN_NAME_INVALID = 'columnNameInvalid'; + const ERROR_CODE_ATTRIBUTE_NOT_VALID = 'attributeNotInvalid'; + const ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicateUniqueAttribute'; + const ERROR_CODE_ILLEGAL_CHARACTERS = 'illegalCharacters'; + const ERROR_CODE_INVALID_ATTRIBUTE = 'invalidAttributeName'; + const ERROR_CODE_WRONG_QUOTES = 'wrongQuotes'; + const ERROR_CODE_COLUMNS_NUMBER = 'wrongColumnsNumber'; + const ERROR_EXCEEDED_MAX_LENGTH = 'exceededMaxLength'; + const ERROR_INVALID_ATTRIBUTE_TYPE = 'invalidAttributeType'; + const ERROR_INVALID_ATTRIBUTE_OPTION = 'absentAttributeOption'; + + protected $errorMessageTemplates = [ + self::ERROR_CODE_SYSTEM_EXCEPTION => 'General system exception happened', + self::ERROR_CODE_COLUMN_NOT_FOUND => 'We can\'t find required columns: %s.', + self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'Columns number: "%s" have empty headers', + self::ERROR_CODE_COLUMN_NAME_INVALID => 'Column names: "%s" are invalid', + self::ERROR_CODE_ATTRIBUTE_NOT_VALID => "Please correct the value for '%s'", + self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE => "Duplicate Unique Attribute for '%s'", + self::ERROR_CODE_ILLEGAL_CHARACTERS => "Illegal character used for attribute %s", + self::ERROR_CODE_INVALID_ATTRIBUTE => 'Header contains invalid attribute(s): "%s"', + self::ERROR_CODE_WRONG_QUOTES => "Curly quotes used instead of straight quotes", + self::ERROR_CODE_COLUMNS_NUMBER => "Number of columns does not correspond to the number of rows in the header", + self::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length', + self::ERROR_INVALID_ATTRIBUTE_TYPE => + 'Value for \'%s\' attribute contains incorrect value, acceptable values are in %s format', + self::ERROR_INVALID_ATTRIBUTE_OPTION => + "Value for %s attribute contains incorrect value, see acceptable values on settings specified for Admin", + ]; + /**#@-*/ /** @@ -57,32 +91,30 @@ abstract class AbstractEntity protected $_dataValidated = false; /** - * DB data source model + * Valid column names * - * @var \Magento\ImportExport\Model\Resource\Import\Data + * @array */ - protected $_dataSourceModel; + protected $validColumnNames = []; /** - * Error codes with arrays of corresponding row numbers + * If we should check column names * - * @var array + * @var bool */ - protected $_errors = []; + protected $needColumnCheck = false; /** - * Error counter + * DB data source model * - * @var int + * @var \Magento\ImportExport\Model\Resource\Import\Data */ - protected $_errorsCount = 0; + protected $_dataSourceModel; /** - * Limit of errors after which pre-processing will exit - * - * @var int + * @var ProcessingErrorAggregatorInterface */ - protected $_errorsLimit = 100; + protected $errorAggregator; /** * Flag to disable import @@ -91,27 +123,6 @@ abstract class AbstractEntity */ protected $_importAllowed = true; - /** - * Array of invalid rows numbers - * - * @var array - */ - protected $_invalidRows = []; - - /** - * Validation failure message template definitions - * - * @var array - */ - protected $_messageTemplates = []; - - /** - * Notice messages - * - * @var string[] - */ - protected $_notices = []; - /** * Magento string lib * @@ -159,7 +170,7 @@ abstract class AbstractEntity * * @var bool */ - protected $logInHistory = false; + protected $logInHistory = true; /** * Rows which will be skipped during import @@ -239,12 +250,34 @@ abstract class AbstractEntity */ protected $_scopeConfig; + /** + * Count if created items + * + * @var int + */ + protected $countItemsCreated = 0; + + /** + * Count if updated items + * + * @var int + */ + protected $countItemsUpdated = 0; + + /** + * Count if deleted items + * + * @var int + */ + protected $countItemsDeleted = 0; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper - * @param Resource $resource + * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param array $data * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -254,6 +287,7 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, array $data = [] ) { $this->_scopeConfig = $scopeConfig; @@ -280,6 +314,20 @@ public function __construct( static::XML_PATH_BUNCH_SIZE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) : 0); + + $this->errorAggregator = $errorAggregator; + + foreach ($this->errorMessageTemplates as $errorCode => $message) { + $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); + } + } + + /** + * @return ProcessingErrorAggregatorInterface + */ + public function getErrorAggregator() + { + return $this->errorAggregator; } /** @@ -319,6 +367,25 @@ protected function _prepareRowForDb(array $rowData) return $rowData; } + /** + * Add errors to error aggregator + * + * @param string $code + * @param array|mixed $errors + * @return void + */ + protected function addErrors($code, $errors) + { + if ($errors) { + $this->getErrorAggregator()->addError( + $code, + ProcessingError::ERROR_LEVEL_CRITICAL, + null, + implode('", "', $errors) + ); + } + } + /** * Validate data rows and save bunches to DB * @@ -351,11 +418,28 @@ protected function _saveValidatedBunches() $startNewBunch = false; } if ($source->valid()) { - // errors limit check - if ($this->_errorsCount >= $this->_errorsLimit) { - return $this; + $valid = true; + try { + $rowData = $source->current(); + foreach ($rowData as $attrName => $element) { + if (!mb_check_encoding($element, 'UTF-8')) { + $valid = false; + $this->addRowError( + AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS, + $this->_processedRowsCount, + $attrName + ); + } + } + } catch (\InvalidArgumentException $e) { + $valid = false; + $this->addRowError($e->getMessage(), $this->_processedRowsCount); + } + if (!$valid) { + $this->_processedRowsCount++; + $source->next(); + continue; } - $rowData = $source->current(); if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) { /* Add entity group that passed validation to bunch */ @@ -390,20 +474,33 @@ protected function _saveValidatedBunches() } /** - * Add error with corresponding current data source row number + * Add error with corresponding current data source row number. * * @param string $errorCode Error code or simply column name - * @param int $errorRowNum Row number - * @param string $columnName OPTIONAL Column name + * @param int $errorRowNum Row number. + * @param string $colName OPTIONAL Column name. + * @param string $errorMessage OPTIONAL Column name. + * @param string $errorLevel + * @param string $errorDescription * @return $this */ - public function addRowError($errorCode, $errorRowNum, $columnName = null) - { + public function addRowError( + $errorCode, + $errorRowNum, + $colName = null, + $errorMessage = null, + $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL, + $errorDescription = null + ) { $errorCode = (string)$errorCode; - $this->_errors[$errorCode][] = [$errorRowNum + 1, $columnName]; - // one added for human readability - $this->_invalidRows[$errorRowNum] = true; - $this->_errorsCount++; + $this->getErrorAggregator()->addError( + $errorCode, + $errorLevel, + $errorRowNum, + $colName, + $errorMessage, + $errorDescription + ); return $this; } @@ -417,7 +514,7 @@ public function addRowError($errorCode, $errorRowNum, $columnName = null) */ public function addMessageTemplate($errorCode, $message) { - $this->_messageTemplates[$errorCode] = $message; + $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); return $this; } @@ -471,66 +568,6 @@ public static function getDefaultBehavior() return \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE; } - /** - * Returns error information grouped by error types and translated (if possible) - * - * @return array - */ - public function getErrorMessages() - { - $messages = []; - foreach ($this->_errors as $errorCode => $errorRows) { - if (isset($this->_messageTemplates[$errorCode])) { - $errorCode = (string)__($this->_messageTemplates[$errorCode]); - } - foreach ($errorRows as $errorRowData) { - $key = $errorRowData[1] ? sprintf($errorCode, $errorRowData[1]) : $errorCode; - $messages[$key][] = $errorRowData[0]; - } - } - return $messages; - } - - /** - * Returns error counter value - * - * @return int - */ - public function getErrorsCount() - { - return $this->_errorsCount; - } - - /** - * Returns error limit value - * - * @return int - */ - public function getErrorsLimit() - { - return $this->_errorsLimit; - } - - /** - * Returns invalid rows count - * - * @return int - */ - public function getInvalidRowsCount() - { - return count($this->_invalidRows); - } - - /** - * Returns model notices - * - * @return string[] - */ - public function getNotices() - { - return $this->_notices; - } - /** * Returns number of checked entities * @@ -606,30 +643,37 @@ public function getMasterAttributeCode() */ public function isAttributeValid($attributeCode, array $attributeParams, array $rowData, $rowNumber) { + $message = ''; switch ($attributeParams['type']) { case 'varchar': $value = $this->string->cleanString($rowData[$attributeCode]); $valid = $this->string->strlen($value) < self::DB_MAX_VARCHAR_LENGTH; + $message = self::ERROR_EXCEEDED_MAX_LENGTH; break; case 'decimal': $value = trim($rowData[$attributeCode]); $valid = (double)$value == $value && is_numeric($value); + $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'select': case 'multiselect': $valid = isset($attributeParams['options'][strtolower($rowData[$attributeCode])]); + $message = self::ERROR_INVALID_ATTRIBUTE_OPTION; break; case 'int': $value = trim($rowData[$attributeCode]); $valid = (int)$value == $value && is_numeric($value); + $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'datetime': $value = trim($rowData[$attributeCode]); $valid = strtotime($value) !== false; + $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'text': $value = $this->string->cleanString($rowData[$attributeCode]); $valid = $this->string->strlen($value) < self::DB_MAX_TEXT_LENGTH; + $message = self::ERROR_EXCEEDED_MAX_LENGTH; break; default: $valid = true; @@ -637,10 +681,17 @@ public function isAttributeValid($attributeCode, array $attributeParams, array $ } if (!$valid) { - $this->addRowError(__("Please correct the value for '%s'."), $rowNumber, $attributeCode); + if ($message == self::ERROR_INVALID_ATTRIBUTE_TYPE) { + $message = sprintf( + $this->errorMessageTemplates[$message], + $attributeCode, + $attributeParams['type'] + ); + } + $this->addRowError($message, $rowNumber, $attributeCode); } elseif (!empty($attributeParams['is_unique'])) { if (isset($this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]])) { - $this->addRowError(__("Duplicate Unique Attribute for '%s'"), $rowNumber, $attributeCode); + $this->addRowError(self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNumber, $attributeCode); return false; } $this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]] = true; @@ -648,17 +699,6 @@ public function isAttributeValid($attributeCode, array $attributeParams, array $ return (bool)$valid; } - /** - * Check that is all of data valid - * - * @return bool - */ - public function isDataValid() - { - $this->validateData(); - return 0 == $this->getErrorsCount(); - } - /** * Import possibility getter * @@ -729,25 +769,22 @@ public function setSource(AbstractSource $source) /** * Validate data * - * @return $this + * @return ProcessingErrorAggregatorInterface * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function validateData() { if (!$this->_dataValidated) { + $this->getErrorAggregator()->clear(); // do all permanent columns exist? $absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames()); - if ($absentColumns) { - throw new \Magento\Framework\Exception\LocalizedException( - __('We can\'t find required columns: %1.', implode(', ', $absentColumns)) - ); - } + $this->addErrors(self::ERROR_CODE_COLUMN_NOT_FOUND, $absentColumns); // check attribute columns names validity $columnNumber = 0; $emptyHeaderColumns = []; $invalidColumns = []; + $invalidAttributes = []; foreach ($this->getSource()->getColNames() as $columnName) { $columnNumber++; if (!$this->isAttributeParticular($columnName)) { @@ -755,27 +792,66 @@ public function validateData() $emptyHeaderColumns[] = $columnNumber; } elseif (!preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) { $invalidColumns[] = $columnName; + } elseif ($this->needColumnCheck && !in_array($columnName, $this->validColumnNames)) { + $invalidAttributes[] = $columnName; } } } + $this->addErrors(self::ERROR_CODE_INVALID_ATTRIBUTE, $invalidAttributes); + $this->addErrors(self::ERROR_CODE_COLUMN_EMPTY_HEADER, $emptyHeaderColumns); + $this->addErrors(self::ERROR_CODE_COLUMN_NAME_INVALID, $invalidColumns); - if ($emptyHeaderColumns) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Columns number: "%1" have empty headers', implode('", "', $emptyHeaderColumns)) - ); + if (!$this->getErrorAggregator()->getErrorsCount()) { + $this->_saveValidatedBunches(); + $this->_dataValidated = true; } - if ($invalidColumns) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Column names: "%1" are invalid', implode('", "', $invalidColumns)) - ); - } - - // initialize validation related attributes - $this->_errors = []; - $this->_invalidRows = []; - $this->_saveValidatedBunches(); - $this->_dataValidated = true; } + return $this->getErrorAggregator(); + } + + /** + * Get count of created items + * + * @return int + */ + public function getCreatedItemsCount() + { + return $this->countItemsCreated; + } + + /** + * Get count of updated items + * + * @return int + */ + public function getUpdatedItemsCount() + { + return $this->countItemsUpdated; + } + + /** + * Get count of deleted items + * + * @return int + */ + public function getDeletedItemsCount() + { + return $this->countItemsDeleted; + } + + /** + * Update proceed items counter + * + * @param array $created + * @param array $updated + * @param array $deleted + * @return $this + */ + protected function updateItemsCounterStats(array $created = [], array $updated = [], array $deleted = []) + { + $this->countItemsCreated = count($created); + $this->countItemsUpdated = count($updated); + $this->countItemsDeleted = count($deleted); return $this; } } diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractSource.php b/app/code/Magento/ImportExport/Model/Import/AbstractSource.php index 05922175aa09a..d91b394a5c080 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractSource.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractSource.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\AbstractEntity; + /** * Data source with columns for Magento_ImportExport */ @@ -38,6 +40,11 @@ abstract class AbstractSource implements \SeekableIterator */ protected $_key = -1; + /** + * @var bool + */ + protected $_foundWrongQuoteFlag = false; + /** * Get and validate column names * @@ -77,7 +84,11 @@ public function current() { $row = $this->_row; if (count($row) != $this->_colQty) { - $row = array_pad($this->_row, $this->_colQty, ''); + if ($this->_foundWrongQuoteFlag) { + throw new \InvalidArgumentException(AbstractEntity::ERROR_CODE_WRONG_QUOTES); + } else { + throw new \InvalidArgumentException(AbstractEntity::ERROR_CODE_COLUMNS_NUMBER); + } } return array_combine($this->_colNames, $row); } diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEav.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEav.php index 38d488a52e752..2e9d08c3e4178 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEav.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEav.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Import\Entity; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; + /** * Import EAV entity abstract model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -78,6 +80,7 @@ abstract class AbstractEav extends \Magento\ImportExport\Model\Import\AbstractEn * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\App\Resource $resource + * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory * @param \Magento\Eav\Model\Config $eavConfig @@ -91,12 +94,13 @@ public function __construct( \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, \Magento\Framework\App\Resource $resource, + ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, array $data = [] ) { - parent::__construct($string, $scopeConfig, $importFactory, $resourceHelper, $resource, $data); + parent::__construct($string, $scopeConfig, $importFactory, $resourceHelper, $resource, $errorAggregator, $data); $this->_storeManager = $storeManager; $this->_attributeCollection = isset( @@ -176,6 +180,7 @@ protected function _initAttributes() 'type' => \Magento\ImportExport\Model\Import::getAttributeType($attribute), 'options' => $this->getAttributeOptions($attribute), ]; + $this->validColumnNames[] = $attribute->getAttributeCode(); } return $this; } diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php index 078e81c60aaa2..79c12cd16a7c4 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php @@ -8,6 +8,8 @@ use Magento\Framework\App\Resource; use Magento\ImportExport\Model\Import\AbstractSource; use Magento\ImportExport\Model\Import as ImportExport; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** * Import entity abstract model @@ -27,6 +29,37 @@ abstract class AbstractEntity const DB_MAX_TEXT_LENGTH = 65536; + const ERROR_CODE_SYSTEM_EXCEPTION = 'systemException'; + const ERROR_CODE_COLUMN_NOT_FOUND = 'columnNotFound'; + const ERROR_CODE_COLUMN_EMPTY_HEADER = 'columnEmptyHeader'; + const ERROR_CODE_COLUMN_NAME_INVALID = 'columnNameInvalid'; + const ERROR_CODE_ATTRIBUTE_NOT_VALID = 'attributeNotInvalid'; + const ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicateUniqueAttribute'; + const ERROR_CODE_ILLEGAL_CHARACTERS = 'illegalCharacters'; + const ERROR_CODE_INVALID_ATTRIBUTE = 'invalidAttributeName'; + const ERROR_CODE_WRONG_QUOTES = 'wrongQuotes'; + const ERROR_CODE_COLUMNS_NUMBER = 'wrongColumnsNumber'; + + protected $errorMessageTemplates = [ + self::ERROR_CODE_SYSTEM_EXCEPTION => 'General system exception happened', + self::ERROR_CODE_COLUMN_NOT_FOUND => 'We can\'t find required columns: %s.', + self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'Columns number: "%s" have empty headers', + self::ERROR_CODE_COLUMN_NAME_INVALID => 'Column names: "%s" are invalid', + self::ERROR_CODE_ATTRIBUTE_NOT_VALID => "Please correct the value for '%s'.", + self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE => "Duplicate Unique Attribute for '%s'", + self::ERROR_CODE_ILLEGAL_CHARACTERS => "Illegal character used for attribute %s", + self::ERROR_CODE_INVALID_ATTRIBUTE => 'Header contains invalid attribute(s): "%s"', + self::ERROR_CODE_WRONG_QUOTES => "Curly quotes used instead of straight quotes", + self::ERROR_CODE_COLUMNS_NUMBER => "Number of columns does not correspond to the number of rows in the header", + ]; + + /** + * Validation failure message template definitions + * + * @var array + */ + protected $_messageTemplates = []; + /** * DB connection. * @@ -35,46 +68,46 @@ abstract class AbstractEntity protected $_connection; /** - * Has data process validation done? + * Has data process validation done?8 * * @var bool */ protected $_dataValidated = false; /** - * DB data source model. + * Valid column names * - * @var \Magento\ImportExport\Model\Resource\Import\Data + * @array */ - protected $_dataSourceModel; + protected $validColumnNames = []; /** - * Entity type id. + * If we should check column names * - * @var int + * @var bool */ - protected $_entityTypeId; + protected $needColumnCheck = false; /** - * Error codes with arrays of corresponding row numbers. + * DB data source model. * - * @var array + * @var \Magento\ImportExport\Model\Resource\Import\Data */ - protected $_errors = []; + protected $_dataSourceModel; /** - * Error counter. + * Entity type id. * * @var int */ - protected $_errorsCount = 0; + protected $_entityTypeId; /** - * Limit of errors after which pre-processing will exit. + * Error codes with arrays of corresponding row numbers. * - * @var int + * @var array */ - protected $_errorsLimit = 100; + protected $_errors = []; /** * Flag to disable import. @@ -90,27 +123,6 @@ abstract class AbstractEntity */ protected $_indexValueAttributes = []; - /** - * Array of invalid rows numbers. - * - * @var array - */ - protected $_invalidRows = []; - - /** - * Validation failure message template definitions. - * - * @var array - */ - protected $_messageTemplates = []; - - /** - * Notice messages. - * - * @var string[] - */ - protected $_notices = []; - /** * Entity model parameters. * @@ -146,17 +158,6 @@ abstract class AbstractEntity */ protected $_processedRowsCount = 0; - /** - * Rows to skip. Valid rows but we have some reasons to skip them. - * - * [Row number 1] => true, - * ... - * [Row number N] => true - * - * @var array - */ - protected $_rowsToSkip = []; - /** * Array of numbers of validated rows as keys and boolean TRUE as values. * @@ -232,6 +233,11 @@ abstract class AbstractEntity */ protected $logInHistory = false; + /** + * @var ProcessingErrorAggregatorInterface + */ + protected $errorAggregator; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -240,6 +246,8 @@ abstract class AbstractEntity * @param Resource $resource * @param \Magento\ImportExport\Model\Resource\Helper $resourceHelper * @param \Magento\Framework\Stdlib\StringUtils $string + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, @@ -248,12 +256,18 @@ public function __construct( \Magento\Eav\Model\Config $config, Resource $resource, \Magento\ImportExport\Model\Resource\Helper $resourceHelper, - \Magento\Framework\Stdlib\StringUtils $string + \Magento\Framework\Stdlib\StringUtils $string, + ProcessingErrorAggregatorInterface $errorAggregator ) { $this->jsonHelper = $jsonHelper; $this->_importExportData = $importExportData; $this->_resourceHelper = $resourceHelper; $this->string = $string; + $this->errorAggregator = $errorAggregator; + + foreach ($this->errorMessageTemplates as $errorCode => $message) { + $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); + } $entityType = $config->getEntityType($this->getEntityTypeCode()); @@ -317,6 +331,25 @@ protected function _prepareRowForDb(array $rowData) return $rowData; } + /** + * Add errors to error aggregator + * + * @param string $code + * @param array|mixed $errors + * @return void + */ + protected function addErrors($code, $errors) + { + if ($errors) { + $this->getErrorAggregator()->addError( + $code, + ProcessingError::ERROR_LEVEL_CRITICAL, + null, + implode('", "', $errors) + ); + } + } + /** * Validate data rows and save bunches to DB. * @@ -346,11 +379,14 @@ protected function _saveValidatedBunches() $nextRowBackup = []; } if ($source->valid()) { - if ($this->_errorsCount >= $this->_errorsLimit) { - // errors limit check - return; + try { + $rowData = $source->current(); + } catch (\InvalidArgumentException $e) { + $this->addRowError($e->getMessage(), $this->_processedRowsCount); + $this->_processedRowsCount++; + $source->next(); + continue; } - $rowData = $source->current(); $this->_processedRowsCount++; @@ -381,15 +417,28 @@ protected function _saveValidatedBunches() * @param string $errorCode Error code or simply column name * @param int $errorRowNum Row number. * @param string $colName OPTIONAL Column name. + * @param string $errorMessage OPTIONAL Column name. + * @param string $errorLevel + * @param string $errorDescription * @return $this */ - public function addRowError($errorCode, $errorRowNum, $colName = null) - { + public function addRowError( + $errorCode, + $errorRowNum, + $colName = null, + $errorMessage = null, + $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL, + $errorDescription = null + ) { $errorCode = (string)$errorCode; - $this->_errors[$errorCode][] = [$errorRowNum + 1, $colName]; - // one added for human readability - $this->_invalidRows[$errorRowNum] = true; - $this->_errorsCount++; + $this->getErrorAggregator()->addError( + $errorCode, + $errorLevel, + $errorRowNum, + $colName, + $errorMessage, + $errorDescription + ); return $this; } @@ -403,7 +452,7 @@ public function addRowError($errorCode, $errorRowNum, $colName = null) */ public function addMessageTemplate($errorCode, $message) { - $this->_messageTemplates[$errorCode] = $message; + $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); return $this; } @@ -485,66 +534,6 @@ public function getEntityTypeId() return $this->_entityTypeId; } - /** - * Returns error information grouped by error types and translated (if possible). - * - * @return array - */ - public function getErrorMessages() - { - $messages = []; - foreach ($this->_errors as $errorCode => $errorRows) { - if (isset($this->_messageTemplates[$errorCode])) { - $errorCode = (string)__($this->_messageTemplates[$errorCode]); - } - foreach ($errorRows as $errorRowData) { - $key = $errorRowData[1] ? sprintf($errorCode, $errorRowData[1]) : $errorCode; - $messages[$key][] = $errorRowData[0]; - } - } - return $messages; - } - - /** - * Returns error counter value. - * - * @return int - */ - public function getErrorsCount() - { - return $this->_errorsCount; - } - - /** - * Returns error limit value. - * - * @return int - */ - public function getErrorsLimit() - { - return $this->_errorsLimit; - } - - /** - * Returns invalid rows count. - * - * @return int - */ - public function getInvalidRowsCount() - { - return count($this->_invalidRows); - } - - /** - * Returns model notices. - * - * @return string[] - */ - public function getNotices() - { - return $this->_notices; - } - /** * Returns number of checked entities. * @@ -643,10 +632,10 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ } if (!$valid) { - $this->addRowError(__("Please correct the value for '%s'."), $rowNum, $attrCode); + $this->addRowError(self::ERROR_CODE_ATTRIBUTE_NOT_VALID, $rowNum, $attrCode); } elseif (!empty($attrParams['is_unique'])) { if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]])) { - $this->addRowError(__("Duplicate Unique Attribute for '%s'"), $rowNum, $attrCode); + $this->addRowError(self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNum, $attrCode); return false; } $this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] = true; @@ -654,17 +643,6 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ return (bool)$valid; } - /** - * Is all of data valid? - * - * @return bool - */ - public function isDataValid() - { - $this->validateData(); - return 0 == $this->_errorsCount; - } - /** * Import possibility getter. * @@ -684,7 +662,22 @@ public function isImportAllowed() */ public function isRowAllowedToImport(array $rowData, $rowNum) { - return $this->validateRow($rowData, $rowNum) && !isset($this->_rowsToSkip[$rowNum]); + $this->validateRow($rowData, $rowNum); + return !$this->getErrorAggregator()->isRowInvalid($rowNum); + } + + /** + * Retrieve message template + * + * @param string $errorCode + * @return null|string + */ + public function retrieveMessageTemplate($errorCode) + { + if (isset($this->_messageTemplates[$errorCode])) { + return $this->_messageTemplates[$errorCode]; + } + return null; } /** @@ -718,6 +711,16 @@ public function setParameters(array $params) return $this; } + /** + * Get data from outside to change behavior. I.e. for setting some default parameters etc. + * + * @return array $params + */ + public function getParameters() + { + return $this->_parameters; + } + /** * Source model setter. * @@ -735,25 +738,24 @@ public function setSource(AbstractSource $source) /** * Validate data. * - * @return $this + * @return ProcessingErrorAggregatorInterface * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function validateData() { if (!$this->_dataValidated) { + $this->getErrorAggregator()->clear(); // do all permanent columns exist? - if ($absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames())) { - throw new \Magento\Framework\Exception\LocalizedException( - __('We can\'t find required columns: %1.', implode(', ', $absentColumns)) - ); - } + $absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames()); + $this->addErrors(self::ERROR_CODE_COLUMN_NOT_FOUND, $absentColumns); if (ImportExport::BEHAVIOR_DELETE != $this->getBehavior()) { // check attribute columns names validity $columnNumber = 0; $emptyHeaderColumns = []; $invalidColumns = []; + $invalidAttributes = []; foreach ($this->getSource()->getColNames() as $columnName) { $columnNumber++; if (!$this->isAttributeParticular($columnName)) { @@ -761,29 +763,30 @@ public function validateData() $emptyHeaderColumns[] = $columnNumber; } elseif (!preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) { $invalidColumns[] = $columnName; + } elseif ($this->needColumnCheck && !in_array($columnName, $this->validColumnNames)) { + $invalidAttributes[] = $columnName; } } } - - if ($emptyHeaderColumns) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Columns number: "%1" have empty headers', implode('", "', $emptyHeaderColumns)) - ); - } - if ($invalidColumns) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Column names: "%1" are invalid', implode('", "', $invalidColumns)) - ); - } + $this->addErrors(self::ERROR_CODE_INVALID_ATTRIBUTE, $invalidAttributes); + $this->addErrors(self::ERROR_CODE_COLUMN_EMPTY_HEADER, $emptyHeaderColumns); + $this->addErrors(self::ERROR_CODE_COLUMN_NAME_INVALID, $invalidColumns); } - // initialize validation related attributes - $this->_errors = []; - $this->_invalidRows = []; - $this->_saveValidatedBunches(); - $this->_dataValidated = true; + if (!$this->getErrorAggregator()->getErrorsCount()) { + $this->_saveValidatedBunches(); + $this->_dataValidated = true; + } } - return $this; + return $this->getErrorAggregator(); + } + + /** + * @return ProcessingErrorAggregatorInterface + */ + public function getErrorAggregator() + { + return $this->errorAggregator; } /** diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingError.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingError.php new file mode 100644 index 0000000000000..c86984ebc4d73 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingError.php @@ -0,0 +1,121 @@ +errorCode = $errorCode; + $this->errorLevel = $errorLevel; + $this->rowNumber = $rowNumber; + $this->columnName = $columnName; + $this->errorMessage = $errorMessage; + $this->errorDescription = $errorDescription; + } + + /** + * @return string + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * @return string + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * @return int + */ + public function getRowNumber() + { + return $this->rowNumber; + } + + /** + * @return string + */ + public function getColumnName() + { + return $this->columnName; + } + + /** + * @return string + */ + public function getErrorLevel() + { + return $this->errorLevel; + } + + /** + * @return string + */ + public function getErrorDescription() + { + return $this->errorDescription; + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php new file mode 100644 index 0000000000000..acb19a9847212 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -0,0 +1,363 @@ +errorFactory = $errorFactory; + } + + /** + * @param string $errorCode + * @param string $errorLevel + * @param int|null $rowNumber + * @param string|null $columnName + * @param string|null $errorMessage + * @param string|null $errorDescription + * @return $this + */ + public function addError( + $errorCode, + $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL, + $rowNumber = null, + $columnName = null, + $errorMessage = null, + $errorDescription = null + ) { + if ($this->isErrorAlreadyAdded($rowNumber, $errorCode)) { + return $this; + } + $this->processErrorStatistics($errorLevel); + $this->processInvalidRow($rowNumber); + $errorMessage = $this->getErrorMessage($errorCode, $errorMessage, $columnName); + + /** @var ProcessingError $newError */ + $newError = $this->errorFactory->create(); + $newError->init($errorCode, $errorLevel, $rowNumber, $columnName, $errorMessage, $errorDescription); + $this->items[] = $newError; + + return $this; + } + + /** + * @param int $rowNumber + * @return $this + */ + public function addRowToSkip($rowNumber) + { + $rowNumber = (int)$rowNumber; + if (!in_array($rowNumber, $this->skippedRows)) { + $this->skippedRows[] = $rowNumber; + } + + return $this; + } + + /** + * @param int $rowNumber + * @return $this + */ + protected function processInvalidRow($rowNumber) + { + if (null !== $rowNumber) { + $rowNumber = (int)$rowNumber; + if (!in_array($rowNumber, $this->invalidRows)) { + $this->invalidRows[] = $rowNumber; + } + } + + return $this; + } + + /** + * @param string $code + * @param string $template + * @return $this + */ + public function addErrorMessageTemplate($code, $template) + { + $this->messageTemplate[$code] = $template; + + return $this; + } + + /** + * @param int $rowNumber + * @return bool + */ + public function isRowInvalid($rowNumber) + { + return in_array((int)$rowNumber, array_merge($this->invalidRows, $this->skippedRows)); + } + + /** + * @return int + */ + public function getInvalidRowsCount() + { + return count($this->invalidRows); + } + + /** + * @param string $validationStrategy + * @param int $allowedErrorCount + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function initValidationStrategy($validationStrategy, $allowedErrorCount = 0) + { + $allowedStrategy = [ + self::VALIDATION_STRATEGY_STOP_ON_ERROR, + self::VALIDATION_STRATEGY_SKIP_ERRORS + ]; + if (!in_array($validationStrategy, $allowedStrategy)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('ImportExport: Import Data validation - Validation strategy not found') + ); + } + $this->validationStrategy = $validationStrategy; + $this->allowedErrorsCount = (int)$allowedErrorCount; + + return $this; + } + + /** + * @return bool + */ + public function hasToBeTerminated() + { + return $this->hasFatalExceptions() || $this->isErrorLimitExceeded(); + } + + /** + * @return bool + */ + public function isErrorLimitExceeded() + { + $isExceeded = false; + $errorsCount = $this->getErrorsCount([ProcessingError::ERROR_LEVEL_NOT_CRITICAL]); + if ($errorsCount > 0 + && $this->validationStrategy == self::VALIDATION_STRATEGY_STOP_ON_ERROR + && $errorsCount >= $this->allowedErrorsCount + ) { + $isExceeded = true; + } + + return $isExceeded; + } + + /** + * @return bool + */ + public function hasFatalExceptions() + { + return (bool)$this->getErrorsCount([ProcessingError::ERROR_LEVEL_CRITICAL]); + } + + + /** + * @return ProcessingError[] + */ + public function getAllErrors() + { + return $this->items; + } + + /** + * @param string[] $codes + * @return ProcessingError[] + */ + public function getErrorsByCode(array $codes) + { + $result = []; + foreach ($this->items as $error) { + if (in_array($error->getErrorCode(), $codes)) { + $result[] = $error; + } + } + + return $result; + } + + /** + * @param int $rowNumber + * @return ProcessingError[] + */ + public function getErrorByRowNumber($rowNumber) + { + $result = []; + foreach ($this->items as $error) { + if ($error->getRowNumber() == (int)$rowNumber) { + $result[] = $error; + } + } + + return $result; + } + + /** + * @param array $errorCode + * @param array $excludedCodes + * @param bool $replaceCodeWithMessage + * @return array + */ + public function getRowsGroupedByErrorCode( + array $errorCode = [], + array $excludedCodes = [], + $replaceCodeWithMessage = true + ) { + $result = []; + foreach ($this->items as $error) { + if ((!empty($errorCode) && in_array($error->getErrorCode(), $errorCode)) + || in_array($error->getErrorCode(), $excludedCodes) + ) { + continue; + } + $message = $replaceCodeWithMessage ? $error->getErrorMessage() : $error->getErrorCode(); + $result[$message][] = $error->getRowNumber()+1; + } + return $result; + } + + /** + * @return int + */ + public function getAllowedErrorsCount() + { + return $this->allowedErrorsCount; + } + + /** + * @param string[] $errorLevels + * @return int + */ + public function getErrorsCount( + array $errorLevels = [ + ProcessingError::ERROR_LEVEL_CRITICAL, + ProcessingError::ERROR_LEVEL_NOT_CRITICAL + ] + ) { + $result = 0; + foreach ($errorLevels as $errorLevel) { + $result += isset($this->errorStatistics[$errorLevel]) ? $this->errorStatistics[$errorLevel] : 0; + } + + return $result; + } + + /** + * @return $this + */ + public function clear() + { + $this->items = []; + $this->errorStatistics = []; + $this->invalidRows = []; + + return $this; + } + + /** + * @param int $rowNum + * @param string $errorCode + * @return bool + */ + protected function isErrorAlreadyAdded($rowNum, $errorCode) + { + $errors = $this->getErrorsByCode([$errorCode]); + foreach ($errors as $error) { + if ($rowNum == $error->getRowNumber()) { + return true; + } + } + return false; + } + + /** + * @param string $errorCode + * @param string $errorMessage + * @param string $columnName + * @return string + */ + protected function getErrorMessage($errorCode, $errorMessage, $columnName) + { + if (null === $errorMessage && isset($this->messageTemplate[$errorCode])) { + $errorMessage = (string)__($this->messageTemplate[$errorCode]); + } + if ($columnName && $errorMessage) { + $errorMessage = sprintf($errorMessage, $columnName); + } + if (!$errorMessage) { + $errorMessage = $errorCode; + } + + return $errorMessage; + } + + /** + * @param string $errorLevel + * @return $this + */ + protected function processErrorStatistics($errorLevel) + { + if (!empty($errorLevel)) { + isset($this->errorStatistics[$errorLevel]) ? + $this->errorStatistics[$errorLevel]++ : $this->errorStatistics[$errorLevel] = 1; + } + + return $this; + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregatorInterface.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregatorInterface.php new file mode 100644 index 0000000000000..3bbf90a068a08 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregatorInterface.php @@ -0,0 +1,163 @@ +_file->readCsv(0, $this->_delimiter, $this->_enclosure); + $parsed = $this->_file->readCsv(0, $this->_delimiter, $this->_enclosure); + if (is_array($parsed) && count($parsed) != $this->_colQty) { + foreach ($parsed as $element) { + if (strpos($element, "'") !== false) { + $this->_foundWrongQuoteFlag = true; + break; + } + } + } else { + $this->_foundWrongQuoteFlag = false; + } + return $parsed; } /** diff --git a/app/code/Magento/ImportExport/Model/Report/Csv.php b/app/code/Magento/ImportExport/Model/Report/Csv.php new file mode 100644 index 0000000000000..387e57e4eefd9 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Report/Csv.php @@ -0,0 +1,151 @@ +reportHelper = $reportHelper; + $this->sourceCsvFactory = $sourceCsvFactory; + $this->outputCsvFactory = $outputCsvFactory; + $this->filesystem = $filesystem; + } + + /** + * @param string $originalFileName + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @param bool $writeOnlyErrorItems + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function createReport( + $originalFileName, + ProcessingErrorAggregatorInterface $errorAggregator, + $writeOnlyErrorItems = false + ) { + $sourceCsv = $this->createSourceCsvModel($originalFileName); + + $outputFileName = $this->generateOutputFileName($originalFileName); + $outputCsv = $this->createOutputCsvModel($outputFileName); + + $columnsName = $sourceCsv->getColNames(); + array_push($columnsName, self::REPORT_ERROR_COLUMN_NAME); + $outputCsv->setHeaderCols($columnsName); + + foreach ($sourceCsv as $rowNum => $rowData) { + $errorMessages = $this->retrieveErrorMessagesByRowNumber($rowNum, $errorAggregator); + if (!$writeOnlyErrorItems || ($writeOnlyErrorItems && $errorMessages)) { + $rowData[self::REPORT_ERROR_COLUMN_NAME] = $errorMessages; + $outputCsv->writeRow($rowData); + } + } + + return $outputFileName; + } + + /** + * @param int $rowNumber + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + public function retrieveErrorMessagesByRowNumber($rowNumber, ProcessingErrorAggregatorInterface $errorAggregator) + { + $messages = ''; + foreach ($errorAggregator->getErrorByRowNumber((int)$rowNumber) as $error) { + $messages .= $error->getErrorMessage() . ','; + } + $messages = rtrim($messages, ','); + + if ($messages) { + $messages = str_pad($messages, 1, '"', STR_PAD_BOTH); + } + + return $messages; + } + + /** + * @param string $sourceFile + * @return string + */ + protected function generateOutputFileName($sourceFile) + { + $fileName = basename($sourceFile, self::ERROR_REPORT_FILE_EXTENSION); + return $fileName . self::ERROR_REPORT_FILE_SUFFIX . self::ERROR_REPORT_FILE_EXTENSION; + } + + /** + * @param string $sourceFile + * @return \Magento\ImportExport\Model\Import\Source\Csv + */ + protected function createSourceCsvModel($sourceFile) + { + return $this->sourceCsvFactory->create( + [ + 'file' => $sourceFile, + 'directory' => $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR) + ] + ); + } + + /** + * @param string $outputFileName + * @return \Magento\ImportExport\Model\Export\Adapter\Csv + */ + protected function createOutputCsvModel($outputFileName) + { + return $this->outputCsvFactory->create( + [ + 'destination' => Import::IMPORT_HISTORY_DIR . $outputFileName, + 'destinationDirectoryCode' => DirectoryList::VAR_DIR, + ] + ); + } +} diff --git a/app/code/Magento/ImportExport/Model/Report/ReportProcessorInterface.php b/app/code/Magento/ImportExport/Model/Report/ReportProcessorInterface.php new file mode 100644 index 0000000000000..891270eb89c60 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Report/ReportProcessorInterface.php @@ -0,0 +1,27 @@ + $this->_linkTable], 'link_table.user_id = main_table.user_id', ['username'] - )->where('execution_time != ?', History::IMPORT_VALIDATION); + )->where( + 'execution_time != ? OR (error_file != "" AND execution_time = ?)', + History::IMPORT_VALIDATION, + History::IMPORT_VALIDATION + ); return $this; } diff --git a/app/code/Magento/ImportExport/Model/Source/Import/AbstractBehavior.php b/app/code/Magento/ImportExport/Model/Source/Import/AbstractBehavior.php index 691fcbaa61c19..2155dc20716d2 100644 --- a/app/code/Magento/ImportExport/Model/Source/Import/AbstractBehavior.php +++ b/app/code/Magento/ImportExport/Model/Source/Import/AbstractBehavior.php @@ -39,9 +39,21 @@ public function toOptionArray() /** * Get current behaviour group code - * + *; * @abstract * @return string */ abstract public function getCode(); + + /** + * Get array of notes for possible values + * + * @param string $entityCode + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getNotes($entityCode) + { + return []; + } } diff --git a/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php b/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php index 4b64cd12d5171..f24eb32dc5084 100644 --- a/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php +++ b/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php @@ -29,4 +29,15 @@ public function getCode() { return 'basic'; } + + /** + * {@inheritdoc} + */ + public function getNotes($entityCode) + { + $messages = ['catalog_product' => [ + \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE => __("Note: Product IDs will be regenerated.") + ]]; + return isset($messages[$entityCode]) ? $messages[$entityCode] : []; + } } diff --git a/app/code/Magento/ImportExport/Setup/UpgradeSchema.php b/app/code/Magento/ImportExport/Setup/UpgradeSchema.php new file mode 100644 index 0000000000000..ae0999e99326b --- /dev/null +++ b/app/code/Magento/ImportExport/Setup/UpgradeSchema.php @@ -0,0 +1,38 @@ +startSetup(); + $installer->getConnection()->addColumn( + $installer->getTable('import_history'), + 'error_file', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 255, + 'nullable' => false, + 'comment' => 'Imported file with errors' + ] + ); + $installer->endSetup(); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/AbstractImportTestCase.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/AbstractImportTestCase.php new file mode 100644 index 0000000000000..f4aee3f827ac6 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/AbstractImportTestCase.php @@ -0,0 +1,44 @@ +objectManagerHelper = new ObjectManagerHelper($this); + } + + /** + * @param array|null $methods + * @return ProcessingErrorAggregatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getErrorAggregatorObject($methods = null) + { + $errorFactory = $this->getMockBuilder( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorFactory' + )->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $errorFactory->method('create')->willReturn( + $this->objectManagerHelper->getObject('Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError') + ); + return $this->getMockBuilder('Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator') + ->setMethods($methods) + ->setConstructorArgs(['errorFactory' => $errorFactory]) + ->getMock(); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/AbstractTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/AbstractTest.php index e4ffe0d900975..b1bb4e6192522 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/AbstractTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/AbstractTest.php @@ -9,7 +9,9 @@ */ namespace Magento\ImportExport\Test\Unit\Model\Import\Entity; -class AbstractTest extends \PHPUnit_Framework_TestCase +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; + +class AbstractTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * Abstract import entity model @@ -22,14 +24,13 @@ protected function setUp() { parent::setUp(); - $this->_model = $this->getMockForAbstractClass( - 'Magento\ImportExport\Model\Import\Entity\AbstractEntity', - [], - '', - false, - true, - true, - ['_saveValidatedBunches'] + $this->_model = $this->getMockBuilder('Magento\ImportExport\Model\Import\Entity\AbstractEntity') + ->disableOriginalConstructor() + ->setMethods(['_saveValidatedBunches', 'getErrorAggregator']) + ->getMockForAbstractClass(); + + $this->_model->method('getErrorAggregator')->willReturn( + $this->getErrorAggregatorObject() ); } @@ -68,13 +69,15 @@ protected function _createSourceAdapterMock(array $columns) * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\Entity\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Columns number: "1" have empty headers */ public function testValidateDataEmptyColumnName() { $this->_createSourceAdapterMock(['']); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertArrayHasKey( + AbstractEntity::ERROR_CODE_COLUMN_EMPTY_HEADER, + $errorAggregator->getRowsGroupedByErrorCode() + ); } /** @@ -86,7 +89,8 @@ public function testValidateDataEmptyColumnNameForDeleteBehaviour() { $this->_createSourceAdapterMock(['']); $this->_model->setParameters(['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE]); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(0, $errorAggregator->getErrorsCount()); } /** @@ -98,33 +102,38 @@ public function testValidateDataColumnNameWithWhitespacesForDeleteBehaviour() { $this->_createSourceAdapterMock([' ']); $this->_model->setParameters(['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE]); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(0, $errorAggregator->getErrorsCount()); } /** * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\Entity\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Columns number: "1" have empty headers */ public function testValidateDataColumnNameWithWhitespaces() { $this->_createSourceAdapterMock([' ']); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertArrayHasKey( + AbstractEntity::ERROR_CODE_COLUMN_EMPTY_HEADER, + $errorAggregator->getRowsGroupedByErrorCode() + ); } /** * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\Entity\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Column names: "_test1" are invalid */ public function testValidateDataAttributeNames() { $this->_createSourceAdapterMock(['_test1']); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertArrayHasKey( + AbstractEntity::ERROR_CODE_COLUMN_NAME_INVALID, + $errorAggregator->getRowsGroupedByErrorCode() + ); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/EavAbstractTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/EavAbstractTest.php index 6add3933dbf53..321f2f455bbdc 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/EavAbstractTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Entity/EavAbstractTest.php @@ -9,7 +9,7 @@ */ namespace Magento\ImportExport\Test\Unit\Model\Import\Entity; -class EavAbstractTest extends \PHPUnit_Framework_TestCase +class EavAbstractTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * Entity type id @@ -60,6 +60,8 @@ class EavAbstractTest extends \PHPUnit_Framework_TestCase protected function setUp() { + parent::setUp(); + $this->_string = new \Magento\Framework\Stdlib\StringUtils(); $scopeConfig = $this->getMock('Magento\Framework\App\Config\ScopeConfigInterface'); @@ -96,6 +98,7 @@ protected function setUp() $this->_importFactory, $this->_resourceHelper, $this->_resource, + $this->getErrorAggregatorObject(), $this->_storeManager, $this->_collectionFactory, $this->_eavConfig, diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/EntityAbstractTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/EntityAbstractTest.php index 21b65012b19db..d60b4c66b9f44 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/EntityAbstractTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/EntityAbstractTest.php @@ -13,7 +13,7 @@ use Magento\ImportExport\Model\Import\AbstractEntity; -class EntityAbstractTest extends \PHPUnit_Framework_TestCase +class EntityAbstractTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** * Abstract import entity model @@ -35,10 +35,12 @@ class EntityAbstractTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->_model = $this->getMockForAbstractClass( - 'Magento\ImportExport\Model\Import\AbstractEntity', - $this->_getModelDependencies() - ); + parent::setUp(); + + $this->_model = $this->getMockBuilder('Magento\ImportExport\Model\Import\AbstractEntity') + ->setConstructorArgs($this->_getModelDependencies()) + ->setMethods(['_saveValidatedBunches']) + ->getMockForAbstractClass(); } protected function tearDown() @@ -65,6 +67,7 @@ protected function _getModelDependencies() 'importFactory' => $importFactory, 'resourceHelper' => $resourceHelper, 'resource' => $resource, + 'errorAggregator' => $this->getErrorAggregatorObject(), 'data' => [ 'data_source_model' => 'not_used', 'connection' => 'not_used', @@ -109,14 +112,14 @@ public function testPrepareRowForDb() */ public function testAddRowError() { - $errorCode = 'error_code '; + $errorCode = 'error_code'; $errorColumnName = 'error_column'; - $this->_model->addRowError($errorCode . '%s', 0, $errorColumnName); + $this->_model->addRowError($errorCode . '%s', 0, $errorColumnName, $errorCode . ' %s'); - $this->assertGreaterThan(0, $this->_model->getErrorsCount()); + $this->assertGreaterThan(0, $this->_model->getErrorAggregator()->getErrorsCount()); - $errors = $this->_model->getErrorMessages(); - $this->assertArrayHasKey($errorCode . $errorColumnName, $errors); + $errors = $this->_model->getErrorAggregator()->getRowsGroupedByErrorCode(); + $this->assertArrayHasKey($errorCode . ' ' . $errorColumnName, $errors); } /** @@ -152,32 +155,11 @@ public function testAddMessageTemplate() $this->_model->addMessageTemplate($errorCode, $message); $this->_model->addRowError($errorCode, 0); - $errors = $this->_model->getErrorMessages(); + $errors = $this->_model->getErrorAggregator()->getRowsGroupedByErrorCode(); $this->assertArrayHasKey($message, $errors); } - /** - * Test for method isDataValid() - */ - public function testIsDataValid() - { - /** @var $model AbstractEntity|\PHPUnit_Framework_MockObject_MockObject */ - $model = $this->getMockForAbstractClass( - 'Magento\ImportExport\Model\Import\AbstractEntity', - [], - '', - false, - true, - true, - ['validateData'] - ); - $model->expects($this->any())->method('validateData'); - $this->assertTrue($model->isDataValid()); - $model->addRowError('test', 1); - $this->assertFalse($model->isDataValid()); - } - /** * Test for method isRowAllowedToImport() */ @@ -417,8 +399,7 @@ public function testIsAttributeValid(array $data) $rowData[$attributeCode] = $data['invalid_value']; $this->assertFalse($this->_model->isAttributeValid($attributeCode, $attributeParams, $rowData, 0)); - - $this->assertEquals(1, $this->_model->getErrorsCount(), 'Wrong count of errors'); + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount(), 'Wrong count of errors'); } /** @@ -525,7 +506,6 @@ protected function _getDataSet($code, $type, $validValue, $invalidValue, $isUniq * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException */ public function testValidateDataPermanentAttributes() { @@ -537,43 +517,45 @@ public function testValidateDataPermanentAttributes() $property->setAccessible(true); $property->setValue($this->_model, $permanentAttributes); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(1, $errorAggregator->getErrorsCount()); } /** * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException */ public function testValidateDataEmptyColumnName() { $this->_createSourceAdapterMock(['']); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(1, $errorAggregator->getErrorsCount()); } /** * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException */ public function testValidateDataColumnNameWithWhitespaces() { $this->_createSourceAdapterMock([' ']); $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(1, $errorAggregator->getErrorsCount()); } /** * Test for method validateData() * * @covers \Magento\ImportExport\Model\Import\AbstractEntity::validateData - * @expectedException \Magento\Framework\Exception\LocalizedException */ public function testValidateDataAttributeNames() { $this->_createSourceAdapterMock(['_test1']); - $this->_model->validateData(); + $errorAggregator = $this->_model->validateData(); + $this->assertEquals(1, $errorAggregator->getErrorsCount()); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php new file mode 100644 index 0000000000000..63b82e3693d2d --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php @@ -0,0 +1,400 @@ +processingErrorFactoryMock = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorFactory', + ['create'], + [], + '', + false + ); + + $this->processingErrorMock1 = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', + null, + [], + '', + false + ); + + $this->processingErrorMock2 = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', + null, + [], + '', + false + ); + + $this->processingErrorMock3 = $this->getMock( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', + null, + [], + '', + false + ); + + $this->processingErrorFactoryMock->expects($this->any())->method('create')->willReturnOnConsecutiveCalls( + $this->processingErrorMock1, + $this->processingErrorMock2, + $this->processingErrorMock3 + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->model = $objectManager->getObject( + '\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator', + [ + 'errorFactory' => $this->processingErrorFactoryMock + ] + ); + } + + /** + * Test for method addError + */ + public function testAddError() + { + $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); + } + + /** + * Test for method addRowToSkip + */ + public function testAddRowToSkip() + { + $this->model->addRowToSkip(7); + $result = $this->model->isRowInvalid(7); + $this->assertTrue($result); + } + + /** + * Test for method addErrorMessageTemplate + */ + public function testAddErrorMessageTemplate() + { + $this->model->addErrorMessageTemplate('columnNotFound', 'Template: No column'); + $this->model->addError('systemException'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', null, 'Description'); + $this->model->addError('columnEmptyHeader', 'not-critical', 4, 'Some column name', 'No header', 'Description'); + $result = $this->model->getRowsGroupedByErrorCode(['systemException']); + $expectedResult = [ + 'Template: No column' => [8], + 'No header' => [5] + ]; + $this->assertEquals($expectedResult, $result); + } + + /** + * Test for method isRowInvalid. Expected true result. + */ + public function testIsRowInvalidTrue() + { + $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); + $result = $this->model->isRowInvalid(7); + $this->assertTrue($result); + } + + /** + * Test for method isRowInvalid. Expected false result. + */ + public function testIsRowInvalidFalse() + { + $this->model->addError('systemException'); + $result = $this->model->isRowInvalid(8); + $this->assertFalse($result); + } + + /** + * Test for method getInvalidRowsCount. Expected 0 invalid rows. + */ + public function testGetInvalidRowsCountZero() + { + $rowsNumber = $this->model->getInvalidRowsCount(); + $this->assertEquals($rowsNumber, 0); + } + + /** + * Test for method getInvalidRowsCount. Expected 1 invalid row. + */ + public function testGetInvalidRowsCountOne() + { + $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); + $rowsNumber = $this->model->getInvalidRowsCount(); + $this->assertEquals($rowsNumber, 1); + } + + /** + * Test for method getInvalidRowsCount. Expected 2 invalid rows. + */ + public function testGetInvalidRowsCountTwo() + { + $this->model->addError('systemException'); + $this->model->addError('systemException', 'critical', 8, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); + $rowsNumber = $this->model->getInvalidRowsCount(); + $this->assertEquals($rowsNumber, 2); + } + + /** + * Test for method initValidationStrategy. + */ + public function testInitValidationStrategy() + { + $this->model->initValidationStrategy('validation-stop-on-errors', 5); + $this->assertEquals(5, $this->model->getAllowedErrorsCount()); + } + + /** + * Test for method initValidationStrategy. + */ + public function testInitValidationStrategyExceed() + { + $this->model->addError('systemException', 'not-critical', 7, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'not-critical', 2, 'Some column name', 'Message', 'Description'); + $this->model->initValidationStrategy('validation-stop-on-errors', 2); + $result = $this->model->isErrorLimitExceeded(); + $this->assertTrue($result); + } + + /** + * Test for method initValidationStrategy. Expected exeption due null incoming parameter + */ + public function testInitValidationStrategyException() + { + $this->setExpectedException('\Magento\Framework\Exception\LocalizedException'); + $this->model->initValidationStrategy(null); + } + + /** + * Test for method isErrorLimitExceeded. Expects error limit exceeded. + */ + public function testIsErrorLimitExceededTrue() + { + $this->model->addError('systemException'); + $this->model->addError('systemException', 'not-critical', 7, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $result = $this->model->isErrorLimitExceeded(); + $this->assertTrue($result); + } + + /** + * Test for method isErrorLimitExceeded. Unexpects error limit exceeded. + */ + public function testIsErrorLimitExceededFalse() + { + $this->model->addError('systemException'); + $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'critical', 4, 'Some column name', 'Message', 'Description'); + $result = $this->model->isErrorLimitExceeded(); + $this->assertFalse($result); + } + + /** + * Test for method hasFatalExceptions. + */ + public function testHasFatalExceptionsTrue() + { + $this->model->addError('systemException'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'Message', 'Description'); + $this->model->addError('columnEmptyHeader', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $result = $this->model->hasFatalExceptions(); + $this->assertTrue($result); + } + + /** + * Test for method hasFatalExceptions. Expects no any fatal exceptions + */ + public function testHasFatalExceptionsFalse() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $result = $this->model->hasFatalExceptions(); + $this->assertFalse($result); + } + + /** + * Test for method hasToBeTerminated. Unexpects any errors and termination. + */ + public function testHasToBeTerminatedFalse() + { + $result = $this->model->hasToBeTerminated(); + $this->assertFalse($result); + } + + /** + * Test for method hasToBeTerminated. Expects errors and execute termination. + */ + public function testHasToBeTerminatedTrue() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $result = $this->model->hasToBeTerminated(); + $this->assertTrue($result); + } + + /** + * Test for method getAllErrors + */ + public function testGetAllErrors() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'not-critical', 5, 'Some column name', 'Message', 'Description'); + $this->model->addError('systemException', 'not-critical', 6, 'Some column name', 'Message', 'Description'); + $result = $this->model->getAllErrors(); + //check if is array of objects + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertInstanceOf('\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', $result[0]); + } + + /** + * Test for method getErrorByRowNumber + */ + public function testGetErrorByRowNumber() + { + $this->model->addError('systemException1', 'not-critical', 1); + $this->model->addError('systemException2', 'not-critical', 1); + $this->model->addError('systemException3', 'not-critical', 2); + $result = $this->model->getErrorByRowNumber(1); + + $this->assertInternalType('array', $result); + $this->assertCount(2, $result); + $this->assertInstanceOf('\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', $result[0]); + $this->assertInstanceOf('\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError', $result[1]); + $this->assertEquals('systemException1', $result[0]->getErrorCode()); + $this->assertEquals('systemException2', $result[1]->getErrorCode()); + } + + /** + * Test logic to prevent adding an identical error more than once. + * The error has to have the same error code for the same row number + */ + public function testAddTheSameErrorTwice() + { + $this->model->addError('systemException', 'not-critical', 1); + $this->model->addError('systemException', 'not-critical', 1); + $result = $this->model->getErrorByRowNumber(1); + + $this->assertCount(1, $result); + $this->assertEquals(1, $this->model->getErrorsCount()); + } + + /** + * Test for method getErrorsByCode. Expects receive errors with code, which present in incomming parameter. + */ + public function testGetErrorsByCodeInArray() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'No column', 'Description'); + $this->model->addError('systemException', 'not-critical', 9, 'Some column name', 'Message', 'Description'); + $result = $this->model->getErrorsByCode(['systemException']); + $this->assertCount(2, $result); + } + + /** + * Test for method getErrorsByCode. Unexpects receive errors with code, which present in incomming parameter. + */ + public function testGetErrorsByCodeNotInArray() + { + $this->model->addError('columnNotFound', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'No column', 'Description'); + $this->model->addError('columnEmptyHeader', 'not-critical', 5, 'Some column name', 'No header', 'Description'); + $result = $this->model->getErrorsByCode(['systemException']); + $this->assertCount(0, $result); + } + + /** + * Test for method getRowsGroupedByErrorCode. Expects errors. + */ + public function testGetRowsGroupedByErrorCodeWithErrors() + { + $this->model->addError('systemException'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'No column', 'Description'); + $this->model->addError('columnEmptyHeader', 'not-critical', 4, 'Some column name', 'No header', 'Description'); + $result = $this->model->getRowsGroupedByErrorCode(['systemException']); + $expectedResult = [ + 'No column' => [8], + 'No header' => [5] + ]; + $this->assertEquals($expectedResult, $result); + } + + /** + * Test for method getRowsGroupedByErrorCode. Unexpects errors. + */ + public function testGetRowsGroupedByErrorCodeNoErrors() + { + $result = $this->model->getRowsGroupedByErrorCode(); + $this->assertInternalType('array', $result); + $this->assertCount(0, $result); + } + + /** + * Test for method getAllowedErrorsCount. + */ + public function testGetAllowedErrorsCount() + { + $this->assertEquals($this->model->getAllowedErrorsCount(), 0); + } + + /** + * Test for method clear. + */ + public function testClear() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'No column', 'Description'); + $this->model->clear(); + $result = $this->model->getAllErrors(); + $this->assertEquals([], $result); + } + + /** + * Test for method getErrorsCount + */ + public function testGetErrorsCount() + { + $this->model->addError('systemException', 'not-critical', 4, 'Some column name', 'Message', 'Description'); + $this->model->addError('columnNotFound', 'critical', 7, 'Some column name', 'No column', 'Description'); + $this->model->addError('systemException', 'not-critical', 9, 'Some column name', 'Message', 'Description'); + $result = $this->model->getErrorsCount(['critical']); + $this->assertEquals($result, 1); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorTest.php new file mode 100644 index 0000000000000..55aa06725d893 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorTest.php @@ -0,0 +1,283 @@ +model = $objectManager->getObject('\Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError'); + } + + /** + * Test for method init. + * + * @dataProvider errorMessageInfo + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function testInit($initData) + { + $errorLevel = isset($initData['errorLevel']) ? $initData['errorLevel'] : null; + $rowNumber = isset($initData['rowNumber']) ? $initData['rowNumber'] : null; + $columnName = isset($initData['columnName']) ? $initData['columnName'] : null; + $errorMessage = isset($initData['errorMessage']) ? $initData['errorMessage'] : null; + $errorDescription = isset($initData['errorDescription']) ? $initData['errorDescription'] : null; + + $this->model->init( + $initData['errorCode'], + $errorLevel, + $rowNumber, + $columnName, + $errorMessage, + $errorDescription + ); + } + + /** + * Data for method testInit + * + * @return array + */ + public function errorMessageInfo() + { + return [ + [ + [ + 'errorCode' => 5, + 'errorLevel' => 'critical', + 'rowNumber' => 7, + 'columnName' => 25, + 'errorMessage' => 'some error message', + 'errorDescription' => 'some error description' + ] + ], + [ + [ + 'errorCode' => 5, + 'errorLevel' => null, + 'rowNumber' => null, + 'columnName' => null, + 'errorMessage' => null, + 'errorDescription' => null + ] + ], + ]; + } + + /** + * Test for method getErrorCode + * + * @dataProvider errorCodeData + */ + public function testGetErrorCode($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getErrorCode(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetErrorCode + * + * @return array + */ + public function errorCodeData() + { + return [ + [ + ['errorCode' => 5], + 5 + ], + [ + ['errorCode' => null], + null + ], + ]; + } + + /** + * Test for method getErrorMessage + * + * @dataProvider errorMessageData + */ + public function testGetErrorMessage($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getErrorMessage(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetErrorMessage + * + * @return array + */ + public function errorMessageData() + { + return [ + [ + ['errorCode' => 5, 'errorMessage' => 'Some error message'], + 'Some error message' + ], + [ + ['errorCode' => 5], + null + ], + ]; + } + + /** + * Test for method getRowNumber + * + * @dataProvider rowNumberData + */ + public function testGetRowNumber($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getRowNumber(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetRowNumber + * + * @return array + */ + public function rowNumberData() + { + return [ + [ + ['errorCode' => 5, 'errorMessage' => 'Some error message', 'rowNumber' => 43], + 43 + ], + [ + ['errorCode' => 5], + null + ], + ]; + } + + /** + * Test for method getColumnName + * + * @dataProvider columnNameData + */ + public function testGetColumnName($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getColumnName(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetColumnName + * + * @return array + */ + public function columnNameData() + { + return [ + [ + [ + 'errorCode' => 5, + 'errorMessage' => 'Some error message', + 'rowNumber' => 43, + 'columnName' => 'Some column name' + ], + 'Some column name' + ], + [ + ['errorCode' => 5], + null + ], + ]; + } + + /** + * Test for method getErrorLevel + * + * @dataProvider errorLevelData + */ + public function testGetErrorLevel($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getErrorLevel(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetErrorLevel + * + * @return array + */ + public function errorLevelData() + { + return [ + [ + [ + 'errorCode' => 5, + 'errorMessage' => 'Some error message', + 'rowNumber' => 43, + 'columnName' => 'Some column name', + 'errorLevel' => 'critical' + ], + 'critical' + ], + [ + ['errorCode' => 5], + null + ], + ]; + } + + /** + * Test for method getErrorDescription + * + * @dataProvider errorDescriptionData + */ + public function testGetErrorDescription($data, $expectedValue) + { + $this->testInit($data); + $result = $this->model->getErrorDescription(); + $this->assertEquals($result, $expectedValue); + } + + /** + * Data for method testGetErrorDescription + * + * @return array + */ + public function errorDescriptionData() + { + return [ + [ + [ + 'errorCode' => 5, + 'errorMessage' => 'Some error message', + 'rowNumber' => 43, + 'columnName' => 'Some column name', + 'errorLevel' => 'critical', + 'errorDescription' => 'Some error description' + ], + 'Some error description' + ], + [ + ['errorCode' => 5], + null + ], + ]; + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/CsvTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/CsvTest.php index 25e6e1a441c08..015809a8d9620 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/CsvTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/CsvTest.php @@ -111,6 +111,10 @@ public function optionalArgsDataProvider() ]; } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage wrongColumnsNumber + */ public function testRewind() { $this->_directoryMock->expects( @@ -139,6 +143,6 @@ public function testRewind() $model->next(); $model->next(); $this->assertSame(2, $model->key()); - $this->assertSame(['column1' => '5', 'column2' => ''], $model->current()); + $model->current(); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/SourceAbstractTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/SourceAbstractTest.php index 225d63b42a06f..ac17ff543661a 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/SourceAbstractTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/SourceAbstractTest.php @@ -43,9 +43,12 @@ public function testGetColNames() $this->assertSame(['key1', 'key2', 'key3'], $this->_model->getColNames()); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage wrongColumnsNumber + */ public function testIteratorInterface() { - $this->assertSame(['key1' => '', 'key2' => '', 'key3' => ''], $this->_model->current()); $this->assertSame(-1, $this->_model->key()); $this->assertFalse($this->_model->valid()); @@ -54,7 +57,7 @@ public function testIteratorInterface() )->method( '_getNextRow' )->will( - $this->onConsecutiveCalls([1, 2, 3], [4, 5], [6, 7, 8], false) + $this->onConsecutiveCalls([1, 2, 3], [4, 5, 5], [6, 7, 8]) ); $data = []; foreach ($this->_model as $key => $value) { @@ -63,11 +66,12 @@ public function testIteratorInterface() $this->assertSame( [ ['key1' => 1, 'key2' => 2, 'key3' => 3], - ['key1' => 4, 'key2' => 5, 'key3' => ''], + ['key1' => 4, 'key2' => 5, 'key3' => 5], ['key1' => 6, 'key2' => 7, 'key3' => 8], ], $data ); + $this->_model->current(); } public function testSeekableInterface() @@ -81,12 +85,12 @@ public function testSeekableInterface() )->method( '_getNextRow' )->will( - $this->onConsecutiveCalls([1, 2, 3], [4, 5], [6, 7, 8], [1, 2, 3], [4, 5]) + $this->onConsecutiveCalls([1, 2, 3], [4, 5, 5], [6, 7, 8], [1, 2, 3], [4, 5, 5]) ); $this->_model->seek(2); $this->assertSame(['key1' => 6, 'key2' => 7, 'key3' => 8], $this->_model->current()); $this->_model->seek(1); - $this->assertSame(['key1' => 4, 'key2' => 5, 'key3' => ''], $this->_model->current()); + $this->assertSame(['key1' => 4, 'key2' => 5, 'key3' => 5], $this->_model->current()); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 66b249b836222..9e65d5516c612 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -11,7 +11,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class ImportTest extends \PHPUnit_Framework_TestCase +class ImportTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { /** @@ -110,6 +110,8 @@ class ImportTest extends \PHPUnit_Framework_TestCase */ public function setUp() { + parent::setUp(); + $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface') ->disableOriginalConstructor() ->getMock(); @@ -126,16 +128,18 @@ public function setUp() ->disableOriginalConstructor() ->setMethods(['getEntityTypeCode', 'getBehavior', 'getEntities']) ->getMockForAbstractClass(); - $this->_entityFactory = $this->getMock( - '\Magento\ImportExport\Model\Import\Entity\Factory', - ['create', 'isNeedToLogInHistory'], - [], - '', - false - ); - $this->getMockBuilder('\Magento\ImportExport\Model\Import\Entity\Factory') + $this->_entityFactory = $this->getMockBuilder('\Magento\ImportExport\Model\Import\Entity\Factory') ->disableOriginalConstructor() ->getMock(); + $this->_entityAdapter = $this->getMockBuilder('\Magento\ImportExport\Model\Import\Entity\AbstractEntity') + ->disableOriginalConstructor() + ->setMethods(['importData', '_saveValidatedBunches', 'getErrorAggregator']) + ->getMockForAbstractClass(); + $this->_entityAdapter->method('getErrorAggregator')->willReturn( + $this->getErrorAggregatorObject(['initValidationStrategy']) + ); + $this->_entityFactory->method('create')->willReturn($this->_entityAdapter); + $this->_importData = $this->getMockBuilder('\Magento\ImportExport\Model\Resource\Import\Data') ->disableOriginalConstructor() ->getMock(); @@ -195,22 +199,17 @@ public function setUp() ]) ->setMethods([ 'getDataSourceModel', - '_getEntityAdapter', 'setData', 'getProcessedEntitiesCount', 'getProcessedRowsCount', - 'getInvalidRowsCount', - 'getErrorsCount', 'getEntity', 'getBehavior', 'isReportEntityType', + '_getEntityAdapter' ]) ->getMock(); $this->setPropertyValue($this->import, '_varDirectory', $this->_varDirectory); - $this->_entityAdapter = $this->getMockBuilder('\Magento\ImportExport\Model\Import\Entity\AbstractEntity') - ->disableOriginalConstructor() - ->setMethods(['importData']) - ->getMockForAbstractClass(); + } /** @@ -238,34 +237,36 @@ public function testImportSource() $this->import->expects($this->any()) ->method('addLogComment') ->with($this->isInstanceOf($phraseClass)); - $this->_entityAdapter->expects($this->once()) + $this->_entityAdapter->expects($this->any()) ->method('importData') ->will($this->returnSelf()); - $this->import->expects($this->once()) + $this->import->expects($this->any()) ->method('_getEntityAdapter') ->will($this->returnValue($this->_entityAdapter)); - + $this->_importConfig + ->expects($this->any()) + ->method('getEntities') + ->willReturn( + [ + $entityTypeCode => [ + 'model' => $entityTypeCode + ] + ] + ); $importOnceMethodsReturnNull = [ - 'getEntity', - 'getBehavior', - 'getProcessedRowsCount', - 'getProcessedEntitiesCount', - 'getInvalidRowsCount', - 'getErrorsCount', + 'getBehavior' ]; foreach ($importOnceMethodsReturnNull as $method) { $this->import->expects($this->once())->method($method)->will($this->returnValue(null)); } - $this->import->importSource(); + $this->assertEquals(true, $this->import->importSource()); } /** * Test importSource with expected exception * - * @expectedException \Magento\Framework\Exception\AlreadyExistsException - * @expectedExceptionMessage URL key for specified store already exists. */ public function testImportSourceException() { @@ -277,7 +278,7 @@ public function testImportSourceException() ->method('getEntityTypeCode') ->will($this->returnValue($entityTypeCode)); $behaviour = 'behaviour'; - $this->_importData->expects($this->once()) + $this->_importData->expects($this->any()) ->method('getBehavior') ->will($this->returnValue($behaviour)); $this->import->expects($this->any()) @@ -291,14 +292,13 @@ public function testImportSourceException() $this->import->expects($this->any()) ->method('addLogComment') ->with($this->isInstanceOf($phraseClass)); - $this->_entityAdapter->expects($this->once()) + $this->_entityAdapter->expects($this->any()) ->method('importData') ->will($this->throwException($exceptionMock)); - $this->import->expects($this->once()) + $this->import->expects($this->any()) ->method('_getEntityAdapter') ->will($this->returnValue($this->_entityAdapter)); $importOnceMethodsReturnNull = [ - 'getEntity', 'getBehavior', ]; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php new file mode 100644 index 0000000000000..ed812fd387504 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php @@ -0,0 +1,114 @@ +reportHelperMock = $this->getMock('\Magento\ImportExport\Helper\Report', [], [], '', false); + + $this->outputCsvFactoryMock = $this->getMock( + '\Magento\ImportExport\Model\Export\Adapter\CsvFactory', + ['create'], + [], + '', + false + ); + $this->outputCsvMock = $this->getMock('\Magento\ImportExport\Model\Export\Adapter\Csv', [], [], '', false); + $this->outputCsvFactoryMock->expects($this->any())->method('create')->willReturn($this->outputCsvMock); + + $this->sourceCsvFactoryMock = $this->getMock( + '\Magento\ImportExport\Model\Import\Source\CsvFactory', + ['create'], + [], + '', + false + ); + $this->sourceCsvMock = $this->getMock('\Magento\ImportExport\Model\Import\Source\Csv', [], [], '', false); + $this->sourceCsvMock->expects($this->any())->method('valid')->willReturnOnConsecutiveCalls(true, true, false); + $this->sourceCsvMock->expects($this->any())->method('current')->willReturnOnConsecutiveCalls( + [23 => 'first error'], + [27 => 'second error'] + ); + $this->sourceCsvFactoryMock->expects($this->any())->method('create')->willReturn($this->sourceCsvMock); + + $this->filesystemMock = $this->getMock('\Magento\Framework\Filesystem', [], [], '', false); + + + $this->csvModel = $objectManager->getObject( + '\Magento\ImportExport\Model\Report\Csv', + [ + 'reportHelper' => $this->reportHelperMock, + 'sourceCsvFactory' => $this->sourceCsvFactoryMock, + 'outputCsvFactory' => $this->outputCsvFactoryMock, + 'filesystem' => $this->filesystemMock + ] + ); + } + + public function testCreateReport() + { + $errorAggregatorMock = $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator', + [], + [], + '', + false + ); + $errorProcessingMock = $this->getMock( + 'Magento\ImportExport\Model\Import\ErrorProcessing', + ['getErrorMessage'], + [], + '', + false + ); + $errorProcessingMock->expects($this->any())->method('getErrorMessage')->willReturn('some_error_message'); + $errorAggregatorMock->expects($this->any())->method('getErrorByRowNumber')->willReturn([$errorProcessingMock]); + $this->sourceCsvMock->expects($this->any())->method('getColNames')->willReturn([]); + + $name = $this->csvModel->createReport('some_file_name', $errorAggregatorMock, true); + + $this->assertEquals($name, 'some_file_name_error_report.csv'); + } +} diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index bf332bd3080a3..88e5915a35a2e 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -8,6 +8,8 @@ + + diff --git a/app/code/Magento/ImportExport/etc/module.xml b/app/code/Magento/ImportExport/etc/module.xml index 4dad36db35038..e072148a1dd4b 100644 --- a/app/code/Magento/ImportExport/etc/module.xml +++ b/app/code/Magento/ImportExport/etc/module.xml @@ -6,6 +6,6 @@ */ --> - + diff --git a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml index 839fdb5f7ecb9..22df8f655b3da 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml +++ b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml @@ -54,6 +54,18 @@ Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Download + + + Error File + type + 0 + 0 + center + action + Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Error + + + Execution Time diff --git a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_import_index.xml b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_import_index.xml index ba7694ea26659..2168ec3cd3416 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_import_index.xml +++ b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_import_index.xml @@ -6,6 +6,9 @@ */ --> + + + diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index c7993391938a7..ec4d7ec59ed64 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -31,6 +31,12 @@ require(['jquery', 'prototype'], function(jQuery){ */ entityBehaviors: getEntityBehaviors() ?>, + /** + * Behaviour notes for import entities + * @type {Array} + */ + entityBehaviorsNotes: getEntityBehaviorsNotes() ?>, + /** * Base url * @type {string} @@ -129,6 +135,25 @@ require(['jquery', 'prototype'], function(jQuery){ this.showUploadFile(false); this.showSampleFile(false); } + this.handleImportBehaviorSelector(); + }, + + /** + * Handle value change in behavior selector + */ + handleImportBehaviorSelector: function() { + var entity = jQuery('#entity'); + if (entity && entity.val()) { + var notes = this.entityBehaviorsNotes[entity.val()]; + var requiredBehavior = this.entityBehaviors[entity.val()]; + var behaviorInput = jQuery('#' + requiredBehavior); + var behavior = behaviorInput && behaviorInput.val(); + if (behavior && notes[behavior]) { + jQuery('#' + requiredBehavior + '-note').html(notes[behavior]); + } else { + jQuery('#' + requiredBehavior + '-note').html(''); + } + } }, /** diff --git a/app/code/Magento/ImportExport/view/adminhtml/web/css/importexport.css b/app/code/Magento/ImportExport/view/adminhtml/web/css/importexport.css new file mode 100644 index 0000000000000..968e2b74fba06 --- /dev/null +++ b/app/code/Magento/ImportExport/view/adminhtml/web/css/importexport.css @@ -0,0 +1,15 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +.import-error-wrapper { + margin-top: 10px; +} + +.import-error-list { + max-height: 120px; + overflow-y: auto; + padding: 10px; + background-color: #fcc; + margin: 10px 0 0 0; +} diff --git a/composer.json b/composer.json index 3d56ffc30ad81..a8a75f38b5e30 100644 --- a/composer.json +++ b/composer.json @@ -109,6 +109,7 @@ "magento/module-dhl": "self.version", "magento/module-directory": "self.version", "magento/module-downloadable": "self.version", + "magento/module-downloadable-import-export": "self.version", "magento/module-eav": "self.version", "magento/module-email": "self.version", "magento/module-encryption-key": "self.version", diff --git a/composer.lock b/composer.lock index 92f5ac69ee2ed..628b9af4df68e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3c94391144a4c2de663dfa465b3a2e8a", + "hash": "de0d450a2c86bda5a2101109be73daff", "packages": [ { "name": "braintree/braintree_php", @@ -121,16 +121,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "1.4.4", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "8dc9b9d85ab639ca60ab4608b34c1279d6ae7bce" + "reference": "a4bee9f4b344b66e0a0d96c7afae1e92edf385fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8dc9b9d85ab639ca60ab4608b34c1279d6ae7bce", - "reference": "8dc9b9d85ab639ca60ab4608b34c1279d6ae7bce", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/a4bee9f4b344b66e0a0d96c7afae1e92edf385fe", + "reference": "a4bee9f4b344b66e0a0d96c7afae1e92edf385fe", "shasum": "" }, "require": { @@ -151,8 +151,8 @@ } }, "autoload": { - "psr-0": { - "JsonSchema": "src/" + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" } }, "notification-url": "https://packagist.org/downloads/", @@ -183,7 +183,7 @@ "json", "schema" ], - "time": "2015-07-14 16:29:50" + "time": "2015-09-08 22:28:04" }, { "name": "magento/composer", @@ -335,7 +335,7 @@ "ZF1", "framework" ], - "time": "2015-06-02 08:04:41" + "time": "2015-02-06 17:25:45" }, { "name": "monolog/monolog", @@ -766,16 +766,16 @@ }, { "name": "symfony/finder", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "ae0f363277485094edc04c9f3cbe595b183b78e4" + "reference": "fff4b0c362640a0ab7355e2647b3d461608e9065" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/ae0f363277485094edc04c9f3cbe595b183b78e4", - "reference": "ae0f363277485094edc04c9f3cbe595b183b78e4", + "url": "https://api.github.com/repos/symfony/Finder/zipball/fff4b0c362640a0ab7355e2647b3d461608e9065", + "reference": "fff4b0c362640a0ab7355e2647b3d461608e9065", "shasum": "" }, "require": { @@ -811,20 +811,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-08-26 17:56:37" }, { "name": "symfony/process", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3" + "reference": "f7b3f73f70a7f8f49a1c838dc3debbf054732d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/48aeb0e48600321c272955132d7606ab0a49adb3", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3", + "url": "https://api.github.com/repos/symfony/Process/zipball/f7b3f73f70a7f8f49a1c838dc3debbf054732d8e", + "reference": "f7b3f73f70a7f8f49a1c838dc3debbf054732d8e", "shasum": "" }, "require": { @@ -860,7 +860,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2015-07-01 11:25:50" + "time": "2015-08-27 06:45:45" }, { "name": "tedivm/jshrink", @@ -958,12 +958,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-code.git", - "reference": "0ed94f842ba60cdc900c46a61bdbd7ac95a3e140" + "reference": "cfd5951ff4348e4430850560416c7ddb755f95d3" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-code/zipball/0ed94f842ba60cdc900c46a61bdbd7ac95a3e140", - "reference": "0ed94f842ba60cdc900c46a61bdbd7ac95a3e140", + "reference": "cfd5951ff4348e4430850560416c7ddb755f95d3", "shasum": "" }, "require": { @@ -972,9 +972,6 @@ }, "require-dev": { "doctrine/common": ">=2.1", - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-stdlib": "self.version" }, "suggest": { @@ -990,7 +987,7 @@ }, "autoload": { "psr-4": { - "Zend\\Code\\": "src/" + "Zend\\Code\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -998,12 +995,12 @@ "BSD-3-Clause" ], "description": "provides facilities to generate arbitrary code using an object oriented interface", - "homepage": "https://github.com/zendframework/zend-code", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "code", "zf2" ], - "time": "2015-03-31 15:39:14" + "time": "2015-04-01 17:59:08" }, { "name": "zendframework/zend-config", @@ -1011,12 +1008,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-config.git", - "reference": "95f3a4b3fa85d49e6f060183122de4596fa6d29d" + "reference": "8682fe4e2923b383bb6472fc84b5796a07589163" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-config/zipball/95f3a4b3fa85d49e6f060183122de4596fa6d29d", - "reference": "95f3a4b3fa85d49e6f060183122de4596fa6d29d", + "reference": "8682fe4e2923b383bb6472fc84b5796a07589163", "shasum": "" }, "require": { @@ -1024,9 +1021,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-filter": "self.version", "zendframework/zend-i18n": "self.version", "zendframework/zend-json": "self.version", @@ -1047,7 +1041,7 @@ }, "autoload": { "psr-4": { - "Zend\\Config\\": "src/" + "Zend\\Config\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1055,12 +1049,12 @@ "BSD-3-Clause" ], "description": "provides a nested object property based user interface for accessing this configuration data within application code", - "homepage": "https://github.com/zendframework/zend-config", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "config", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 17:59:31" }, { "name": "zendframework/zend-console", @@ -1068,23 +1062,18 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-console.git", - "reference": "54823d9ba6f8ce39046384ee5a043b5b3d5f56d7" + "reference": "94ab6663b07e19f20b3319ecf317bd72b6a72dca" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-console/zipball/54823d9ba6f8ce39046384ee5a043b5b3d5f56d7", - "reference": "54823d9ba6f8ce39046384ee5a043b5b3d5f56d7", + "reference": "94ab6663b07e19f20b3319ecf317bd72b6a72dca", "shasum": "" }, "require": { "php": ">=5.3.23", "zendframework/zend-stdlib": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "suggest": { "zendframework/zend-filter": "To support DefaultRouteMatcher usage", "zendframework/zend-validator": "To support DefaultRouteMatcher usage" @@ -1098,19 +1087,19 @@ }, "autoload": { "psr-4": { - "Zend\\Console\\": "src/" + "Zend\\Console\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-console", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "console", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 17:59:48" }, { "name": "zendframework/zend-crypt", @@ -1169,12 +1158,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-di.git", - "reference": "b9f8de081adecf71a003a569e9ba76c0a4c00bf2" + "reference": "0811f2a67ad0b50dfb8d602ed67cde0b82249190" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-di/zipball/b9f8de081adecf71a003a569e9ba76c0a4c00bf2", - "reference": "b9f8de081adecf71a003a569e9ba76c0a4c00bf2", + "reference": "0811f2a67ad0b50dfb8d602ed67cde0b82249190", "shasum": "" }, "require": { @@ -1183,9 +1172,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-servicemanager": "self.version" }, "suggest": { @@ -1200,19 +1186,19 @@ }, "autoload": { "psr-4": { - "Zend\\Di\\": "src/" + "Zend\\Di\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-di", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "di", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:01:30" }, { "name": "zendframework/zend-escaper", @@ -1220,22 +1206,17 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-escaper.git", - "reference": "15e5769e4fcdb4bf07ebd76500810e7070e23a97" + "reference": "65b3328627362b0be1d5e9067bc846511d1fbc96" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/15e5769e4fcdb4bf07ebd76500810e7070e23a97", - "reference": "15e5769e4fcdb4bf07ebd76500810e7070e23a97", + "reference": "65b3328627362b0be1d5e9067bc846511d1fbc96", "shasum": "" }, "require": { "php": ">=5.3.23" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -1245,19 +1226,19 @@ }, "autoload": { "psr-4": { - "Zend\\Escaper\\": "src/" + "Zend\\Escaper\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-escaper", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "escaper", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:02:07" }, { "name": "zendframework/zend-eventmanager", @@ -1265,23 +1246,18 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "58d21c95c7005a527262fd536499195f104e83f9" + "reference": "38df5b567d4ff4d22144745c503ba0502d0d5695" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/58d21c95c7005a527262fd536499195f104e83f9", - "reference": "58d21c95c7005a527262fd536499195f104e83f9", + "reference": "38df5b567d4ff4d22144745c503ba0502d0d5695", "shasum": "" }, "require": { "php": ">=5.3.23", "zendframework/zend-stdlib": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -1291,19 +1267,19 @@ }, "autoload": { "psr-4": { - "Zend\\EventManager\\": "src/" + "Zend\\EventManager\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-event-manager", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "eventmanager", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:05:26" }, { "name": "zendframework/zend-filter", @@ -1311,12 +1287,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "6d8aed2da81b62a04747346c4370562cdbe34595" + "reference": "b13741a88553351fc52472de529b57b580b8f6f1" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/6d8aed2da81b62a04747346c4370562cdbe34595", - "reference": "6d8aed2da81b62a04747346c4370562cdbe34595", + "reference": "b13741a88553351fc52472de529b57b580b8f6f1", "shasum": "" }, "require": { @@ -1324,9 +1300,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-crypt": "self.version", "zendframework/zend-servicemanager": "self.version", "zendframework/zend-uri": "self.version" @@ -1346,7 +1319,7 @@ }, "autoload": { "psr-4": { - "Zend\\Filter\\": "src/" + "Zend\\Filter\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1354,12 +1327,12 @@ "BSD-3-Clause" ], "description": "provides a set of commonly needed data filters", - "homepage": "https://github.com/zendframework/zend-filter", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "filter", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:25" }, { "name": "zendframework/zend-form", @@ -1367,12 +1340,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-form.git", - "reference": "bca0db55718355d25c2c10fdd41a83561f1c94b3" + "reference": "09f5bd46ffbf783df22281898e2175b291bd43a3" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-form/zipball/bca0db55718355d25c2c10fdd41a83561f1c94b3", - "reference": "bca0db55718355d25c2c10fdd41a83561f1c94b3", + "reference": "09f5bd46ffbf783df22281898e2175b291bd43a3", "shasum": "" }, "require": { @@ -1381,9 +1354,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-captcha": "self.version", "zendframework/zend-code": "self.version", "zendframework/zend-eventmanager": "self.version", @@ -1414,19 +1384,19 @@ }, "autoload": { "psr-4": { - "Zend\\Form\\": "src/" + "Zend\\Form\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-form", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "form", "zf2" ], - "time": "2015-03-28 20:29:18" + "time": "2015-04-01 18:09:25" }, { "name": "zendframework/zend-http", @@ -1434,12 +1404,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "9c6047a0bdb3094d3ea07a215ff929cc47de4deb" + "reference": "ee6220609845b32d1b2873c9ac694aef56d508f5" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-http/zipball/9c6047a0bdb3094d3ea07a215ff929cc47de4deb", - "reference": "9c6047a0bdb3094d3ea07a215ff929cc47de4deb", + "reference": "ee6220609845b32d1b2873c9ac694aef56d508f5", "shasum": "" }, "require": { @@ -1449,11 +1419,6 @@ "zendframework/zend-uri": "self.version", "zendframework/zend-validator": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -1463,7 +1428,7 @@ }, "autoload": { "psr-4": { - "Zend\\Http\\": "src/" + "Zend\\Http\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1471,12 +1436,12 @@ "BSD-3-Clause" ], "description": "provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests", - "homepage": "https://github.com/zendframework/zend-http", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "http", "zf2" ], - "time": "2015-03-27 15:46:30" + "time": "2015-04-01 18:09:25" }, { "name": "zendframework/zend-i18n", @@ -1484,12 +1449,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "9aebc5287373a802540d75fe5508417f866c2e52" + "reference": "33051775d9a8c341fe3b77d1f3daa0e921e2f4bd" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/9aebc5287373a802540d75fe5508417f866c2e52", - "reference": "9aebc5287373a802540d75fe5508417f866c2e52", + "reference": "33051775d9a8c341fe3b77d1f3daa0e921e2f4bd", "shasum": "" }, "require": { @@ -1497,9 +1462,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-cache": "self.version", "zendframework/zend-config": "self.version", "zendframework/zend-eventmanager": "self.version", @@ -1528,19 +1490,19 @@ }, "autoload": { "psr-4": { - "Zend\\I18n\\": "src/" + "Zend\\I18n\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-i18n", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "i18n", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:26" }, { "name": "zendframework/zend-inputfilter", @@ -1548,12 +1510,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "4b1398f3635fae3cc5e873c5bb067274f3d10a93" + "reference": "16856fec61f285e41e5492235220a4dec06ab90f" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4b1398f3635fae3cc5e873c5bb067274f3d10a93", - "reference": "4b1398f3635fae3cc5e873c5bb067274f3d10a93", + "reference": "16856fec61f285e41e5492235220a4dec06ab90f", "shasum": "" }, "require": { @@ -1563,9 +1525,6 @@ "zendframework/zend-validator": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-servicemanager": "self.version" }, "suggest": { @@ -1580,19 +1539,19 @@ }, "autoload": { "psr-4": { - "Zend\\InputFilter\\": "src/" + "Zend\\InputFilter\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-input-filter", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "inputfilter", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:09:26" }, { "name": "zendframework/zend-json", @@ -1600,12 +1559,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-json.git", - "reference": "2d845e151c1b9a237cf1899ac31e17fb10bd1e3f" + "reference": "76aeb27e4baf39799e5ca3cf6f2fdd6748ee930c" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-json/zipball/2d845e151c1b9a237cf1899ac31e17fb10bd1e3f", - "reference": "2d845e151c1b9a237cf1899ac31e17fb10bd1e3f", + "reference": "76aeb27e4baf39799e5ca3cf6f2fdd6748ee930c", "shasum": "" }, "require": { @@ -1613,9 +1572,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-http": "self.version", "zendframework/zend-server": "self.version" }, @@ -1633,7 +1589,7 @@ }, "autoload": { "psr-4": { - "Zend\\Json\\": "src/" + "Zend\\Json\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1641,12 +1597,12 @@ "BSD-3-Clause" ], "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", - "homepage": "https://github.com/zendframework/zend-json", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "json", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:26" }, { "name": "zendframework/zend-loader", @@ -1654,22 +1610,17 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-loader.git", - "reference": "65de2c7a56f8eee633c6bf1cfab73e45648880d4" + "reference": "6868b8a0c346f17fb97724c3a63aa2cbf6b94865" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/65de2c7a56f8eee633c6bf1cfab73e45648880d4", - "reference": "65de2c7a56f8eee633c6bf1cfab73e45648880d4", + "reference": "6868b8a0c346f17fb97724c3a63aa2cbf6b94865", "shasum": "" }, "require": { "php": ">=5.3.23" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -1679,19 +1630,19 @@ }, "autoload": { "psr-4": { - "Zend\\Loader\\": "src/" + "Zend\\Loader\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-loader", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "loader", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:09:26" }, { "name": "zendframework/zend-log", @@ -1699,12 +1650,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-log.git", - "reference": "002e3c810cad7e31e51c9895e9e3cb6fbd312cdd" + "reference": "2d5d20fd45470506bdaff727c46dc25fe953146e" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-log/zipball/002e3c810cad7e31e51c9895e9e3cb6fbd312cdd", - "reference": "002e3c810cad7e31e51c9895e9e3cb6fbd312cdd", + "reference": "2d5d20fd45470506bdaff727c46dc25fe953146e", "shasum": "" }, "require": { @@ -1713,9 +1664,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-console": "self.version", "zendframework/zend-db": "self.version", "zendframework/zend-escaper": "self.version", @@ -1739,7 +1687,7 @@ }, "autoload": { "psr-4": { - "Zend\\Log\\": "src/" + "Zend\\Log\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1747,13 +1695,13 @@ "BSD-3-Clause" ], "description": "component for general purpose logging", - "homepage": "https://github.com/zendframework/zend-log", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "log", "logging", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:26" }, { "name": "zendframework/zend-math", @@ -1761,22 +1709,17 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-math.git", - "reference": "f41fe4acfd809c14f2a802d1aa45dec8fcd2cc73" + "reference": "634123f83ca90b6613f132d0d100e6b5e9890a29" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-math/zipball/f41fe4acfd809c14f2a802d1aa45dec8fcd2cc73", - "reference": "f41fe4acfd809c14f2a802d1aa45dec8fcd2cc73", + "reference": "634123f83ca90b6613f132d0d100e6b5e9890a29", "shasum": "" }, "require": { "php": ">=5.3.23" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "suggest": { "ext-bcmath": "If using the bcmath functionality", "ext-gmp": "If using the gmp functionality", @@ -1792,19 +1735,19 @@ }, "autoload": { "psr-4": { - "Zend\\Math\\": "src/" + "Zend\\Math\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-math", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "math", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:09:27" }, { "name": "zendframework/zend-modulemanager", @@ -1812,12 +1755,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-modulemanager.git", - "reference": "af7ae3cd29a1efb73cc66ae1081e606039d5c20f" + "reference": "cbe16b0eafe734a062ed0182381e64b9c953dccf" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-modulemanager/zipball/af7ae3cd29a1efb73cc66ae1081e606039d5c20f", - "reference": "af7ae3cd29a1efb73cc66ae1081e606039d5c20f", + "reference": "cbe16b0eafe734a062ed0182381e64b9c953dccf", "shasum": "" }, "require": { @@ -1826,9 +1769,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-config": "self.version", "zendframework/zend-console": "self.version", "zendframework/zend-loader": "self.version", @@ -1850,19 +1790,19 @@ }, "autoload": { "psr-4": { - "Zend\\ModuleManager\\": "src/" + "Zend\\ModuleManager\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-module-manager", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "modulemanager", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:09:27" }, { "name": "zendframework/zend-mvc", @@ -1870,12 +1810,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-mvc.git", - "reference": "0b4a4a829b30be510a3f215c4ff00c703ee8b431" + "reference": "bfff0f5f9e4d925ee13b8c159c9d6ae7e0db5412" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-mvc/zipball/0b4a4a829b30be510a3f215c4ff00c703ee8b431", - "reference": "0b4a4a829b30be510a3f215c4ff00c703ee8b431", + "reference": "bfff0f5f9e4d925ee13b8c159c9d6ae7e0db5412", "shasum": "" }, "require": { @@ -1886,9 +1826,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-authentication": "self.version", "zendframework/zend-console": "self.version", "zendframework/zend-di": "self.version", @@ -1937,19 +1874,19 @@ }, "autoload": { "psr-4": { - "Zend\\Mvc\\": "src/" + "Zend\\Mvc\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-mvc", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "mvc", "zf2" ], - "time": "2015-03-26 18:55:14" + "time": "2015-04-01 18:09:27" }, { "name": "zendframework/zend-serializer", @@ -1957,12 +1894,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "3c531789a9882a5deb721356a7bd2642b65d4b09" + "reference": "a46960854d6326f0036d98c9abc7a79e36e25928" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/3c531789a9882a5deb721356a7bd2642b65d4b09", - "reference": "3c531789a9882a5deb721356a7bd2642b65d4b09", + "reference": "a46960854d6326f0036d98c9abc7a79e36e25928", "shasum": "" }, "require": { @@ -1972,9 +1909,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-servicemanager": "self.version" }, "suggest": { @@ -1989,7 +1923,7 @@ }, "autoload": { "psr-4": { - "Zend\\Serializer\\": "src/" + "Zend\\Serializer\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1997,12 +1931,12 @@ "BSD-3-Clause" ], "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", - "homepage": "https://github.com/zendframework/zend-serializer", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "serializer", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:28" }, { "name": "zendframework/zend-server", @@ -2010,12 +1944,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-server.git", - "reference": "d11ff0bd529d202022823d4accf5983cbd50fc49" + "reference": "fc73c34490908ba143af3c57c7e166b40c4b9f8e" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-server/zipball/d11ff0bd529d202022823d4accf5983cbd50fc49", - "reference": "d11ff0bd529d202022823d4accf5983cbd50fc49", + "reference": "fc73c34490908ba143af3c57c7e166b40c4b9f8e", "shasum": "" }, "require": { @@ -2023,11 +1957,6 @@ "zendframework/zend-code": "self.version", "zendframework/zend-stdlib": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -2037,19 +1966,19 @@ }, "autoload": { "psr-4": { - "Zend\\Server\\": "src/" + "Zend\\Server\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-server", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "server", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:28" }, { "name": "zendframework/zend-servicemanager", @@ -2057,21 +1986,18 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "57cf99fa5ac08c05a135a8d0d676c52a5e450083" + "reference": "d3c27c708a148a30608f313a5b7a61a531bd9cb9" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/57cf99fa5ac08c05a135a8d0d676c52a5e450083", - "reference": "57cf99fa5ac08c05a135a8d0d676c52a5e450083", + "reference": "d3c27c708a148a30608f313a5b7a61a531bd9cb9", "shasum": "" }, "require": { "php": ">=5.3.23" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-di": "self.version" }, "suggest": { @@ -2087,19 +2013,19 @@ }, "autoload": { "psr-4": { - "Zend\\ServiceManager\\": "src/" + "Zend\\ServiceManager\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-service-manager", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "servicemanager", "zf2" ], - "time": "2015-03-23 18:29:14" + "time": "2015-04-01 18:09:28" }, { "name": "zendframework/zend-soap", @@ -2107,12 +2033,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-soap.git", - "reference": "a599463aba97ce247faf3fb443e3c7858b46449b" + "reference": "e42b900798ea95a9063fa4922da976d8b3a8ab6f" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-soap/zipball/a599463aba97ce247faf3fb443e3c7858b46449b", - "reference": "a599463aba97ce247faf3fb443e3c7858b46449b", + "reference": "e42b900798ea95a9063fa4922da976d8b3a8ab6f", "shasum": "" }, "require": { @@ -2122,9 +2048,6 @@ "zendframework/zend-uri": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-http": "self.version" }, "suggest": { @@ -2139,19 +2062,19 @@ }, "autoload": { "psr-4": { - "Zend\\Soap\\": "src/" + "Zend\\Soap\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-soap", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "soap", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:29" }, { "name": "zendframework/zend-stdlib", @@ -2159,21 +2082,18 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-stdlib.git", - "reference": "cf05c5ba75606e47ffee91cedc72778da46f74c3" + "reference": "eab586f4c18af3fa63c977611939f1f4a3cf1030" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/cf05c5ba75606e47ffee91cedc72778da46f74c3", - "reference": "cf05c5ba75606e47ffee91cedc72778da46f74c3", + "reference": "eab586f4c18af3fa63c977611939f1f4a3cf1030", "shasum": "" }, "require": { "php": ">=5.3.23" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-eventmanager": "self.version", "zendframework/zend-filter": "self.version", "zendframework/zend-serializer": "self.version", @@ -2194,19 +2114,19 @@ }, "autoload": { "psr-4": { - "Zend\\Stdlib\\": "src/" + "Zend\\Stdlib\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-stdlib", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "stdlib", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:29" }, { "name": "zendframework/zend-text", @@ -2214,12 +2134,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-text.git", - "reference": "d962ea25647b20527f3ca34ae225bbc885dabfc7" + "reference": "35f519e20e575a331c2ee554e5a555a59ce4b9e2" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-text/zipball/d962ea25647b20527f3ca34ae225bbc885dabfc7", - "reference": "d962ea25647b20527f3ca34ae225bbc885dabfc7", + "reference": "35f519e20e575a331c2ee554e5a555a59ce4b9e2", "shasum": "" }, "require": { @@ -2227,11 +2147,6 @@ "zendframework/zend-servicemanager": "self.version", "zendframework/zend-stdlib": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -2241,19 +2156,19 @@ }, "autoload": { "psr-4": { - "Zend\\Text\\": "src/" + "Zend\\Text\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-text", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "text", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:29" }, { "name": "zendframework/zend-uri", @@ -2261,12 +2176,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "bd9e625639415376f6a82551c73328448d7bc7d1" + "reference": "53f5b162b293f80de8b951eece8e08be83c4fe16" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/bd9e625639415376f6a82551c73328448d7bc7d1", - "reference": "bd9e625639415376f6a82551c73328448d7bc7d1", + "reference": "53f5b162b293f80de8b951eece8e08be83c4fe16", "shasum": "" }, "require": { @@ -2274,11 +2189,6 @@ "zendframework/zend-escaper": "self.version", "zendframework/zend-validator": "self.version" }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master" - }, "type": "library", "extra": { "branch-alias": { @@ -2288,7 +2198,7 @@ }, "autoload": { "psr-4": { - "Zend\\Uri\\": "src/" + "Zend\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2296,12 +2206,12 @@ "BSD-3-Clause" ], "description": "a component that aids in manipulating and validating » Uniform Resource Identifiers (URIs)", - "homepage": "https://github.com/zendframework/zend-uri", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "uri", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:29" }, { "name": "zendframework/zend-validator", @@ -2309,12 +2219,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "45fac2545a0f2eb66d71cb7966feee481e7c475f" + "reference": "eb678d20256f120a72ca27276bbb2875841701ab" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/45fac2545a0f2eb66d71cb7966feee481e7c475f", - "reference": "45fac2545a0f2eb66d71cb7966feee481e7c475f", + "reference": "eb678d20256f120a72ca27276bbb2875841701ab", "shasum": "" }, "require": { @@ -2322,9 +2232,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-db": "self.version", "zendframework/zend-filter": "self.version", "zendframework/zend-i18n": "self.version", @@ -2352,7 +2259,7 @@ }, "autoload": { "psr-4": { - "Zend\\Validator\\": "src/" + "Zend\\Validator\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2360,12 +2267,12 @@ "BSD-3-Clause" ], "description": "provides a set of commonly needed validators", - "homepage": "https://github.com/zendframework/zend-validator", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "validator", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:30" }, { "name": "zendframework/zend-view", @@ -2373,12 +2280,12 @@ "source": { "type": "git", "url": "https://github.com/zendframework/zend-view.git", - "reference": "37beb1ad46e530f627b4b6c3716efd728e976ba9" + "reference": "e119b4b5f082af58a96eb206e782b62c193227bf" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/zendframework/zend-view/zipball/37beb1ad46e530f627b4b6c3716efd728e976ba9", - "reference": "37beb1ad46e530f627b4b6c3716efd728e976ba9", + "reference": "e119b4b5f082af58a96eb206e782b62c193227bf", "shasum": "" }, "require": { @@ -2388,9 +2295,6 @@ "zendframework/zend-stdlib": "self.version" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "dev-master", "zendframework/zend-authentication": "self.version", "zendframework/zend-escaper": "self.version", "zendframework/zend-feed": "self.version", @@ -2429,7 +2333,7 @@ }, "autoload": { "psr-4": { - "Zend\\View\\": "src/" + "Zend\\View\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2437,12 +2341,12 @@ "BSD-3-Clause" ], "description": "provides a system of helpers, output filters, and variable escaping", - "homepage": "https://github.com/zendframework/zend-view", + "homepage": "https://github.com/zendframework/zf2", "keywords": [ "view", "zf2" ], - "time": "2015-03-25 20:55:48" + "time": "2015-04-01 18:09:30" } ], "packages-dev": [ @@ -3584,16 +3488,16 @@ }, { "name": "symfony/config", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Config.git", - "reference": "6c905bbed1e728226de656e4c07d620dfe9e80d9" + "reference": "5ab9ff48b3cb5b40951a607f77fc1cbfd29edba8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Config/zipball/6c905bbed1e728226de656e4c07d620dfe9e80d9", - "reference": "6c905bbed1e728226de656e4c07d620dfe9e80d9", + "url": "https://api.github.com/repos/symfony/Config/zipball/5ab9ff48b3cb5b40951a607f77fc1cbfd29edba8", + "reference": "5ab9ff48b3cb5b40951a607f77fc1cbfd29edba8", "shasum": "" }, "require": { @@ -3630,20 +3534,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-08-27 06:45:45" }, { "name": "symfony/dependency-injection", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/DependencyInjection.git", - "reference": "851e3ffe8a366b1590bdaf3df2c1395f2d27d8a6" + "reference": "c0a3a97b9450d77cd8eff81c5825efb3624c255b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/DependencyInjection/zipball/851e3ffe8a366b1590bdaf3df2c1395f2d27d8a6", - "reference": "851e3ffe8a366b1590bdaf3df2c1395f2d27d8a6", + "url": "https://api.github.com/repos/symfony/DependencyInjection/zipball/c0a3a97b9450d77cd8eff81c5825efb3624c255b", + "reference": "c0a3a97b9450d77cd8eff81c5825efb3624c255b", "shasum": "" }, "require": { @@ -3690,20 +3594,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "time": "2015-08-24 07:16:32" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3" + "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/b58c916f1db03a611b72dd702564f30ad8fe83fa", + "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa", "shasum": "" }, "require": { @@ -3748,20 +3652,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-06-18 19:21:56" + "time": "2015-08-24 07:13:45" }, { "name": "symfony/filesystem", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Filesystem.git", - "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8" + "reference": "f079e9933799929584200b9a926f72f29e291654" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Filesystem/zipball/2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", - "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", + "url": "https://api.github.com/repos/symfony/Filesystem/zipball/f079e9933799929584200b9a926f72f29e291654", + "reference": "f079e9933799929584200b9a926f72f29e291654", "shasum": "" }, "require": { @@ -3797,20 +3701,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-08-27 07:03:44" }, { "name": "symfony/stopwatch", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Stopwatch.git", - "reference": "b07a866719bbac5294c67773340f97b871733310" + "reference": "abc61bac76fb10ffa2c6373d7932bc35190dbf3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/b07a866719bbac5294c67773340f97b871733310", - "reference": "b07a866719bbac5294c67773340f97b871733310", + "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/abc61bac76fb10ffa2c6373d7932bc35190dbf3b", + "reference": "abc61bac76fb10ffa2c6373d7932bc35190dbf3b", "shasum": "" }, "require": { @@ -3846,20 +3750,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2015-07-01 18:23:16" + "time": "2015-08-24 07:13:45" }, { "name": "symfony/yaml", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" + "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/2dc7b06c065df96cc686c66da2705e5e18aef661", + "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661", "shasum": "" }, "require": { @@ -3895,7 +3799,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "time": "2015-08-24 07:13:45" } ], "aliases": [], diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php index df6a03e4fbc70..a4dd5f80b41fb 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php @@ -110,11 +110,11 @@ public function testConstruct() // check message templates $this->assertAttributeInternalType( 'array', - '_messageTemplates', + 'errorMessageTemplates', $this->_entityAdapter, 'Templates must be an array.' ); - $this->assertAttributeNotEmpty('_messageTemplates', $this->_entityAdapter, 'Templates must not be empty'); + $this->assertAttributeNotEmpty('errorMessageTemplates', $this->_entityAdapter, 'Templates must not be empty'); // check attributes $this->assertAttributeInternalType( @@ -398,7 +398,9 @@ public function testImportDataAddUpdate() $result = $this->_entityAdapter->setSource( \Magento\ImportExport\Model\Import\Adapter::findAdapterFor($sourceFile, $directoryWrite) - )->isDataValid(); + ) + ->validateData() + ->hasToBeTerminated(); $this->assertFalse($result, 'Validation result must be false.'); // import data @@ -492,8 +494,8 @@ public function testImportDataDelete() $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $result = $this->_entityAdapter->setSource( \Magento\ImportExport\Model\Import\Adapter::findAdapterFor($sourceFile, $directoryWrite) - )->isDataValid(); - $this->assertTrue($result, 'Validation result must be true.'); + )->validateData()->hasToBeTerminated(); + $this->assertTrue(!$result, 'Validation result must be true.'); // import data $this->_entityAdapter->importData(); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerCompositeTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerCompositeTest.php index e241fdc28639d..1ac06a6a9110a 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerCompositeTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerCompositeTest.php @@ -6,6 +6,7 @@ namespace Magento\CustomerImportExport\Model\Import; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; class CustomerCompositeTest extends \PHPUnit_Framework_TestCase { @@ -145,19 +146,26 @@ public function testImportData($behavior, $sourceFile, array $dataBefore, array $filesystem = $this->_objectManager->create('Magento\Framework\Filesystem'); $rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $this->_entityAdapter->getErrorAggregator()->initValidationStrategy( + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR, + 10 + ); + // set fixture CSV file $result = $this->_entityAdapter->setSource( \Magento\ImportExport\Model\Import\Adapter::findAdapterFor($sourceFile, $rootDirectory) - )->isDataValid(); + ) + ->validateData() + ->hasToBeTerminated(); if ($errors) { - $this->assertFalse($result); - } else { $this->assertTrue($result); + } else { + $this->assertFalse($result); } // assert validation errors // can't use error codes because entity adapter gathers only error messages from aggregated adapters - $actualErrors = array_values($this->_entityAdapter->getErrorMessages()); + $actualErrors = array_values($this->_entityAdapter->getErrorAggregator()->getRowsGroupedByErrorCode()); $this->assertEquals($errors, $actualErrors); // assert data before import @@ -192,7 +200,7 @@ public function importDataDataProvider() '$sourceFile' => $filesDirectory . self::UPDATE_FILE_NAME, '$dataBefore' => $this->_beforeImport, '$dataAfter' => $this->_afterImport, - '$errors' => [[6]], // row #6 has no website + '$errors' => [], ]; return $sourceData; diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php index 802d203628aaa..e9ff2b4c0045e 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php @@ -46,7 +46,7 @@ protected function setUp() ->create('Magento\CustomerImportExport\Model\Import\Customer'); $this->_model->setParameters(['behavior' => Import::BEHAVIOR_ADD_UPDATE]); - $propertyAccessor = new \ReflectionProperty($this->_model, '_messageTemplates'); + $propertyAccessor = new \ReflectionProperty($this->_model, 'errorMessageTemplates'); $propertyAccessor->setAccessible(true); $propertyAccessor->setValue($this->_model, []); @@ -98,7 +98,8 @@ public function testImportData() $this->_model ->setParameters(['behavior' => Import::BEHAVIOR_ADD_UPDATE]) ->setSource($source) - ->isDataValid(); + ->validateData() + ->hasToBeTerminated(); $this->_model->importData(); @@ -148,7 +149,7 @@ public function testDeleteData() \Magento\TestFramework\Helper\Bootstrap::getInstance() ->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); $source = new \Magento\ImportExport\Model\Import\Source\Csv( - __DIR__ . '/_files/customers_to_import.csv', + __DIR__ . '/_files/customers_to_delete.csv', $this->directoryWrite ); @@ -158,11 +159,9 @@ public function testDeleteData() ); $this->assertEquals(3, $customerCollection->count(), 'Count of existing customers are invalid'); - $this->_model->setParameters( - ['behavior' => Import::BEHAVIOR_DELETE] - )->setSource( - $source - )->isDataValid(); + $this->_model->setParameters(['behavior' => Import::BEHAVIOR_DELETE]) + ->setSource($source) + ->validateData(); $this->_model->importData(); @@ -178,68 +177,75 @@ public function testGetEntityTypeCode() public function testValidateRowDuplicateEmail() { + $this->_model->getErrorAggregator()->clear(); $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(0, $this->_model->getErrorsCount()); + $this->assertEquals(0, $this->_model->getErrorAggregator()->getErrorsCount()); $this->_customerData[Customer::COLUMN_EMAIL] = strtoupper( $this->_customerData[Customer::COLUMN_EMAIL] ); $this->_model->validateRow($this->_customerData, 1); - $this->assertEquals(1, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_DUPLICATE_EMAIL_SITE, - $this->_model->getErrorMessages() + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_DUPLICATE_EMAIL_SITE] + ) ); } public function testValidateRowInvalidEmail() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData[Customer::COLUMN_EMAIL] = 'wrong_email@format'; $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(1, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_INVALID_EMAIL, - $this->_model->getErrorMessages() + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_INVALID_EMAIL] + ) ); } public function testValidateRowInvalidWebsite() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData[Customer::COLUMN_WEBSITE] = 'not_existing_web_site'; $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(1, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_INVALID_WEBSITE, - $this->_model->getErrorMessages() + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_INVALID_WEBSITE] + ) ); } public function testValidateRowInvalidStore() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData[Customer::COLUMN_STORE] = 'not_existing_web_store'; $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(1, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_INVALID_STORE, - $this->_model->getErrorMessages() + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_INVALID_STORE] + ) ); } public function testValidateRowPasswordLengthIncorrect() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData['password'] = '12345'; $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(1, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_PASSWORD_LENGTH, $this->_model->getErrorMessages() + $this->assertEquals(1, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_PASSWORD_LENGTH] + ) ); } public function testValidateRowPasswordLengthCorrect() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData['password'] = '1234567890'; $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(0, $this->_model->getErrorsCount()); + $this->assertEquals(0, $this->_model->getErrorAggregator()->getErrorsCount()); } /** @@ -247,27 +253,33 @@ public function testValidateRowPasswordLengthCorrect() */ public function testValidateRowAttributeRequired() { + $this->_model->getErrorAggregator()->clear(); unset($this->_customerData['firstname']); unset($this->_customerData['lastname']); unset($this->_customerData['group_id']); $this->_model->validateRow($this->_customerData, 0); - $this->assertEquals(0, $this->_model->getErrorsCount()); + $this->assertEquals(0, $this->_model->getErrorAggregator()->getErrorsCount()); $this->_customerData[Customer::COLUMN_EMAIL] = 'new.customer@example.com'; $this->_model->validateRow($this->_customerData, 1); - $this->assertGreaterThan(0, $this->_model->getErrorsCount()); - $this->assertArrayHasKey(Customer::ERROR_VALUE_IS_REQUIRED, $this->_model->getErrorMessages()); + $this->assertGreaterThan(0, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_VALUE_IS_REQUIRED] + ) + ); } public function testValidateEmailForDeleteBehavior() { + $this->_model->getErrorAggregator()->clear(); $this->_customerData[Customer::COLUMN_EMAIL] = 'new.customer@example.com'; $this->_model->setParameters(['behavior' => Import::BEHAVIOR_DELETE]); $this->_model->validateRow($this->_customerData, 0); - $this->assertGreaterThan(0, $this->_model->getErrorsCount()); - $this->assertArrayHasKey( - Customer::ERROR_CUSTOMER_NOT_FOUND, $this->_model->getErrorMessages() + $this->assertGreaterThan(0, $this->_model->getErrorAggregator()->getErrorsCount()); + $this->assertNotEmpty( + $this->_model->getErrorAggregator()->getErrorsByCode([Customer::ERROR_CUSTOMER_NOT_FOUND] + ) ); } } diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/address_import_update.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/address_import_update.csv index 76fbbc72911e6..936184ce967e8 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/address_import_update.csv +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/address_import_update.csv @@ -1,5 +1,4 @@ "_website","_email","_entity_id","city","company","country_id","fax","firstname","lastname","middlename","postcode","prefix","region","region_id","street","suffix","telephone","vat_id","vat_is_valid","vat_request_date","vat_request_id","vat_request_success","_address_default_billing_","_address_default_shipping_" "admin","BetsyParker@example.com",1,,,"US",,"Katy","Parker","T.",19107,,,,"1079 Rocky Road",,"215-629-9720",,,,,,,1 "admin","BetsyParker@example.com",3,"Phoenix",,"US",,"Brad","Brown","H.",85034,,"Arizona",4,"4225 Martha Street",,"928-707-1577",,,,,,1, -"admin","JenniferCJackson@teleworm.us",4,"Tampa",,"US",,"Jennifer ","Jackson","C.",33602,,"Florida",18,"1192 Maryland Avenue",,"727-555-0854",,,,,,, "admin","BetsyParker@example.com",,"Tallahassee",,"US",,"William ","Compton","M.",32301,,"Florida",18,"1973 Drainer Avenue",,"850-590-7403",,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customer_composite_update.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customer_composite_update.csv index 96fa62191dff9..dfe6ee546efd0 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customer_composite_update.csv +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customer_composite_update.csv @@ -4,4 +4,3 @@ "LoriBBanks@magento.com","admin","admin",,"05/06/2012 15:59","Admin",0,,"Lori","Female",1,"Banks","B.","7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz",,,,0,,,0,,"Wenatchee",,"US",,"Lori","Banks",,98801,,"Washington","2573 Goodwin Avenue",,"509-421-4364",,1,1 "BetsyParker@example.com","admin","admin",,"05/06/2012 16:13","Admin",0,,"NotBetsy","Female",1,"NotParker","H.","145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1",,,,0,,,2,,"Philadelphia",,"US",,"Betsy","Parker",,19108,,"Pennsylvania","1079 Rocky Road 1079 Rocky Road 2",,"215-629-9720",,1,1 "KellyNIlson@magento.com","base","admin",,"05/06/2012 17:11","Admin",0,,"Kelly","Female",1,"Nilson","H.","145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1",,,,0,,,2,,,,,,,,,,,,,,,,, -"MichaelJackson@magento.com",,"admin",,"05/06/2012 17:11","Admin",0,,"Michael","Male",1,"Jackson","J.","145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1",,,,0,,,2,,,,,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_delete.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_delete.csv new file mode 100644 index 0000000000000..0c7ebed0ec47d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_delete.csv @@ -0,0 +1,4 @@ +email,_website,_store,confirmation,created_at,created_in,default_billing,default_shipping,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,rp_token,rp_token_created_at,store_id,suffix,taxvat,website_id,password +customer@example.com,base,admin,,5/6/2012 16:15,Admin,4,4,0,,Firstname,Male,1,Lastname,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +julie.worrell@example.com,base,admin,,5/6/2012 16:19,Admin,4,4,0,,Julie,Female,1,Worrell,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +david.lamar@example.com,base,admin,,5/6/2012 16:25,Admin,4,4,0,,David,Male,1,Lamar,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/EntityAbstractTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/EntityAbstractTest.php index 6ef46093056d6..7098903cb7e8e 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/EntityAbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/EntityAbstractTest.php @@ -37,7 +37,10 @@ public function testSaveValidatedBunches() $objectManager->get('Magento\Framework\App\Config\ScopeConfigInterface'), $objectManager->get('Magento\ImportExport\Model\ImportFactory'), $objectManager->get('Magento\ImportExport\Model\Resource\Helper'), - $objectManager->get('Magento\Framework\App\Resource') + $objectManager->get('Magento\Framework\App\Resource'), + $objectManager->get( + 'Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface' + ) ], '', true, diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php index 788e55647ad7b..cdc7979569f1b 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model; +use Magento\ImportExport\Model\Import; + /** * @magentoDataFixture Magento/ImportExport/_files/import_data.php */ @@ -13,7 +15,7 @@ class ImportTest extends \PHPUnit_Framework_TestCase /** * Model object which is used for tests * - * @var \Magento\ImportExport\Model\Import + * @var Import */ protected $_model; @@ -31,18 +33,24 @@ class ImportTest extends \PHPUnit_Framework_TestCase 'catalog_product' => [ 'token' => 'Magento\ImportExport\Model\Source\Import\Behavior\Basic', 'code' => 'basic_behavior', + 'notes' => [ + \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE => "Note: Product IDs will be regenerated." + ], ], 'customer_composite' => [ 'token' => 'Magento\ImportExport\Model\Source\Import\Behavior\Basic', 'code' => 'basic_behavior', + 'notes' => [], ], 'customer' => [ 'token' => 'Magento\ImportExport\Model\Source\Import\Behavior\Custom', 'code' => 'custom_behavior', + 'notes' => [], ], 'customer_address' => [ 'token' => 'Magento\ImportExport\Model\Source\Import\Behavior\Custom', 'code' => 'custom_behavior', + 'notes' => [], ], ]; @@ -82,6 +90,10 @@ public function testImportSource() $customersCollection->resetData(); $customersCollection->clear(); + $this->_model->setData( + Import::FIELD_NAME_VALIDATION_STRATEGY, + Import\ErrorProcessing\ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS + ); $this->_model->importSource(); $customers = $customersCollection->getItems(); diff --git a/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php b/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php index 0112fd010caef..a5b6ad9b38dcf 100644 --- a/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php @@ -654,7 +654,13 @@ public function execute() /** @var \Magento\ImportExport\Model\Import $import */ $import = $this->fixtureModel->getObjectManager()->create( 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'catalog_product', 'behavior' => 'append']] + [ + 'data' => [ + 'entity' => 'catalog_product', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ] ); $source = new Generator($pattern, $configurablesCount); diff --git a/setup/src/Magento/Setup/Fixtures/CustomersFixture.php b/setup/src/Magento/Setup/Fixtures/CustomersFixture.php index c2390cd989164..2f65fe6f2301c 100644 --- a/setup/src/Magento/Setup/Fixtures/CustomersFixture.php +++ b/setup/src/Magento/Setup/Fixtures/CustomersFixture.php @@ -91,10 +91,16 @@ public function execute() '_address_default_shipping_' => '1', ]; $generator = new Generator($pattern, $customersNumber); - /** @var Magento\ImportExport\Model\Import $import */ + /** @var \Magento\ImportExport\Model\Import $import */ $import = $this->fixtureModel->getObjectManager()->create( 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'customer_composite', 'behavior' => 'append']] + [ + 'data' => [ + 'entity' => 'customer_composite', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ] ); // it is not obvious, but the validateSource() will actually save import queue data to DB $import->validateSource($generator); diff --git a/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php b/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php index ab977626ac4d6..0c60626964571 100644 --- a/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php @@ -84,7 +84,13 @@ public function execute() /** @var \Magento\ImportExport\Model\Import $import */ $import = $this->fixtureModel->getObjectManager()->create( 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'catalog_product', 'behavior' => 'append']] + [ + 'data' => [ + 'entity' => 'catalog_product', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ] ); // it is not obvious, but the validateSource() will actually save import queue data to DB $import->validateSource($generator); diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/ConfigurableProductsFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/ConfigurableProductsFixtureTest.php index f0cd86010b237..0643f6cd40fec 100644 --- a/setup/src/Magento/Setup/Test/Unit/Fixtures/ConfigurableProductsFixtureTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/ConfigurableProductsFixtureTest.php @@ -80,7 +80,13 @@ public function testExecute() $valueMap = [ [ 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'catalog_product', 'behavior' => 'append']], + [ + 'data' => [ + 'entity' => 'catalog_product', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ], $importMock ], ['Magento\Store\Model\StoreManager', [], $storeManagerMock] diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/CustomersFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/CustomersFixtureTest.php index 6761be9e0dbd5..53d3fc8b63200 100644 --- a/setup/src/Magento/Setup/Test/Unit/Fixtures/CustomersFixtureTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/CustomersFixtureTest.php @@ -52,7 +52,13 @@ public function testExecute() $valueMap = [ [ 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'customer_composite', 'behavior' => 'append']], + [ + 'data' => [ + 'entity' => 'customer_composite', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ], $importMock ], ['Magento\Store\Model\StoreManager', [], $storeManagerMock] diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/SimpleProductsFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/SimpleProductsFixtureTest.php index 497a60768893c..163212dee20ef 100644 --- a/setup/src/Magento/Setup/Test/Unit/Fixtures/SimpleProductsFixtureTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/SimpleProductsFixtureTest.php @@ -80,7 +80,13 @@ public function testExecute() $valueMap = [ [ 'Magento\ImportExport\Model\Import', - ['data' => ['entity' => 'catalog_product', 'behavior' => 'append']], + [ + 'data' => [ + 'entity' => 'catalog_product', + 'behavior' => 'append', + 'validation_strategy' => 'validation-stop-on-errors' + ] + ], $importMock ], ['Magento\Store\Model\StoreManager', [], $storeManagerMock]