diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index 4d65e1df627aa..83bfcbbabc253 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -183,8 +183,9 @@ public function getFinalPrice($qty, $product) $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice); $finalPrice += $this->getTotalBundleItemsPrice($product, $qty); + $finalPrice = max(0, $finalPrice); $product->setFinalPrice($finalPrice); - return max(0, $product->getData('final_price')); + return $finalPrice; } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php index 8fe925252dc42..4ad87969f0489 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php @@ -70,11 +70,11 @@ protected function _prepareForm() } $this->_coreRegistry->register('attribute_type_hidden_fields', $_hiddenFields); - $this->_eventManager->dispatch('product_attribute_form_build_main_tab', ['form' => $form]); - $frontendInputValues = array_merge($frontendInputElm->getValues(), $additionalTypes); $frontendInputElm->setValues($frontendInputValues); + $this->_eventManager->dispatch('product_attribute_form_build_main_tab', ['form' => $form]); + return $this; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php index 5409d2cf73b88..2917708741475 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php @@ -88,7 +88,7 @@ protected function createActionPage($title = null) /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); if ($this->getRequest()->getParam('popup')) { - if ($this->getRequest()->getParam('product_tab') == 'variations') { + if ($this->getRequest()->getParam('product_tab') === 'variations') { $resultPage->addHandle(['popup', 'catalog_product_attribute_edit_product_tab_variations_popup']); } else { $resultPage->addHandle(['popup', 'catalog_product_attribute_edit_popup']); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php index 7ad9c3f4d888d..805800c7bc2ac 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php @@ -16,7 +16,7 @@ class StockDataFilter /** * The greatest value which could be stored in CatalogInventory Qty field */ - const MAX_QTY_VALUE = 99999999.9999; + const MAX_QTY_VALUE = 99999999; /** * @var ScopeConfigInterface diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index d1f91a90e105c..8769df70dbb7a 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -142,11 +142,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ protected $optionInstance; - /** - * @var bool - */ - protected $optionsInitialized = false; - /** * @var array */ @@ -1901,6 +1896,7 @@ public function addOption(Product\Option $option) { $options = (array)$this->getData('options'); $options[] = $option; + $option->setProduct($this); $this->setData('options', $options); return $this; } diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index c97d660f5a829..38032a1c2c5d2 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogInventory\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\CatalogInventory\Api\StockRegistryInterface; @@ -213,6 +214,7 @@ private function prepareMeta() 'validation' => [ 'validate-number' => true, 'validate-digits' => true, + 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, ], 'imports' => [ 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal', diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index 309db72472c37..dea1bc2f4c20a 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -96,6 +96,7 @@ true true + 99999999 200 [GLOBAL] diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js index b10711671d502..b14527c9c9ec6 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js +++ b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js @@ -21,6 +21,7 @@ define([ this.validation['validate-number'] = !isDigits; this.validation['validate-digits'] = isDigits; + this.validation['less-than-equals-to'] = isDigits ? 99999999 : 99999999.9999; this.validate(); } }); diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index bff84a9236564..8c46242abb110 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -84,6 +84,12 @@ protected function _getItemsData() ->getProductCollection(); $optionsFacetedData = $productCollection->getFacetedData($attribute->getAttributeCode()); + if (count($optionsFacetedData) === 0 + && $this->getAttributeIsFilterable($attribute) !== static::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS + ) { + return $this->itemDataBuilder->build(); + } + $productSize = $productCollection->getSize(); $options = $attribute->getFrontend() @@ -100,9 +106,8 @@ protected function _getItemsData() : 0; // Check filter type if ( - $count === 0 - && $this->getAttributeIsFilterable($attribute) === static::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS - && !$this->isOptionReducesResults($count, $productSize) + $this->getAttributeIsFilterable($attribute) === static::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS + && (!$this->isOptionReducesResults($count, $productSize) || $count === 0) ) { continue; } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 1d3749f4a95a9..9a1e4bbcf1b4c 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext; +use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\Framework\DB\Select; use Magento\Framework\Exception\StateException; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; @@ -398,14 +399,17 @@ public function getFacetedData($field) $this->_renderFilters(); $result = []; $aggregations = $this->searchResult->getAggregations(); - $bucket = $aggregations->getBucket($field . '_bucket'); - if ($bucket) { - foreach ($bucket->getValues() as $value) { - $metrics = $value->getMetrics(); - $result[$metrics['value']] = $metrics; + // This behavior is for case with empty object when we got EmptyRequestDataException + if (null !== $aggregations) { + $bucket = $aggregations->getBucket($field . RequestGenerator::BUCKET_SUFFIX); + if ($bucket) { + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result[$metrics['value']] = $metrics; + } + } else { + throw new StateException(__('Bucket does not exist')); } - } else { - throw new StateException(__('Bucket do not exists')); } return $result; } diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php index 98facef4679c2..d4f371e965e07 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php @@ -17,6 +17,9 @@ use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Store\Model\StoreManagerInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TableMapper { /** @@ -108,7 +111,7 @@ private function getMappingData(FilterInterface $filter) && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) ) { $table = $this->resource->getTableName('catalog_product_index_eav'); - $alias = $field . '_filter'; + $alias = $field . RequestGenerator::FILTER_SUFFIX; $mapOn = sprintf( 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d', $alias, @@ -118,7 +121,7 @@ private function getMappingData(FilterInterface $filter) $mappedFields = []; } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) { $table = $attribute->getBackendTable(); - $alias = $field . '_filter'; + $alias = $field . RequestGenerator::FILTER_SUFFIX; $mapOn = 'search_index.entity_id = ' . $alias . '.entity_id'; $mappedFields = null; } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php index fae8db405ea3c..4afe01f845f94 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php @@ -6,6 +6,7 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Layer\Filter; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -104,9 +105,6 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getAttributeCode', 'getFrontend', 'getIsFilterable']) ->getMock(); - $this->attribute->expects($this->atLeastOnce()) - ->method('getFrontend') - ->will($this->returnValue($this->frontend)); $this->request = $this->getMockBuilder('\Magento\Framework\App\RequestInterface') ->setMethods(['getParam']) @@ -143,6 +141,9 @@ public function testApplyFilter() $this->attribute->expects($this->exactly(2)) ->method('getAttributeCode') ->will($this->returnValue($attributeCode)); + $this->attribute->expects($this->atLeastOnce()) + ->method('getFrontend') + ->will($this->returnValue($this->frontend)); $this->target->setAttributeModel($this->attribute); @@ -202,6 +203,9 @@ public function testGetItemsWithApply() $this->attribute->expects($this->exactly(2)) ->method('getAttributeCode') ->will($this->returnValue($attributeCode)); + $this->attribute->expects($this->atLeastOnce()) + ->method('getFrontend') + ->will($this->returnValue($this->frontend)); $this->target->setAttributeModel($this->attribute); @@ -283,6 +287,9 @@ public function testGetItemsWithoutApply() $this->attribute->expects($this->exactly(2)) ->method('getAttributeCode') ->will($this->returnValue($attributeCode)); + $this->attribute->expects($this->atLeastOnce()) + ->method('getFrontend') + ->will($this->returnValue($this->frontend)); $this->target->setAttributeModel($this->attribute); @@ -329,6 +336,105 @@ public function testGetItemsWithoutApply() $this->assertEquals($expectedFilterItems, $result); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetItemsOnlyWithResults() + { + $attributeCode = 'attributeCode'; + $selectedOptions = [ + [ + 'label' => 'selectedOptionLabel1', + 'value' => 'selectedOptionValue1', + ], + [ + 'label' => 'selectedOptionLabel2', + 'value' => 'selectedOptionValue2', + ], + ]; + $facetedData = [ + 'selectedOptionValue1' => ['count' => 10], + 'selectedOptionValue2' => ['count' => 0], + ]; + $builtData = [ + [ + 'label' => $selectedOptions[0]['label'], + 'value' => $selectedOptions[0]['value'], + 'count' => $facetedData[$selectedOptions[0]['value']]['count'], + ], + ]; + + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->attribute->expects($this->atLeastOnce()) + ->method('getIsFilterable') + ->willReturn(AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS); + $this->attribute->expects($this->atLeastOnce()) + ->method('getFrontend') + ->will($this->returnValue($this->frontend)); + + $this->target->setAttributeModel($this->attribute); + + $this->frontend->expects($this->once()) + ->method('getSelectOptions') + ->willReturn($selectedOptions); + + $this->fulltextCollection->expects($this->once()) + ->method('getFacetedData') + ->willReturn($facetedData); + $this->fulltextCollection->expects($this->once()) + ->method('getSize') + ->will($this->returnValue(50)); + + $this->itemDataBuilder->expects($this->once()) + ->method('addItemData') + ->with( + $selectedOptions[0]['label'], + $selectedOptions[0]['value'], + $facetedData[$selectedOptions[0]['value']]['count'] + ) + ->will($this->returnSelf()); + + $this->itemDataBuilder->expects($this->once()) + ->method('build') + ->willReturn($builtData); + + $expectedFilterItems = [ + $this->createFilterItem(0, $builtData[0]['label'], $builtData[0]['value'], $builtData[0]['count']), + ]; + $result = $this->target->getItems(); + + $this->assertEquals($expectedFilterItems, $result); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetItemsIfFacetedDataIsEmpty() + { + $attributeCode = 'attributeCode'; + + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->attribute->expects($this->atLeastOnce()) + ->method('getIsFilterable') + ->willReturn(0); + + $this->target->setAttributeModel($this->attribute); + + $this->fulltextCollection->expects($this->once()) + ->method('getFacetedData') + ->willReturn([]); + + $this->itemDataBuilder->expects($this->once()) + ->method('build') + ->willReturn([]); + + $this->assertEquals([], $this->target->getItems()); + } + /** * @param int $index * @param string $label diff --git a/app/code/Magento/ConfigurableProduct/Observer/HideUnsupportedAttributeTypes.php b/app/code/Magento/ConfigurableProduct/Observer/HideUnsupportedAttributeTypes.php new file mode 100644 index 0000000000000..4ccbab7e1af05 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Observer/HideUnsupportedAttributeTypes.php @@ -0,0 +1,68 @@ +supportedTypes = $supportedTypes; + $this->request = $request; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ + public function execute(EventObserver $observer) + { + if (!$this->isVariationsPopupUsed()) { + return; + } + + /** @var \Magento\Framework\Data\Form $form */ + $form = $observer->getForm(); + + $filteredValues = []; + /** @var \Magento\Framework\Data\Form\Element\Select $frontendInput */ + $frontendInput = $form->getElement('frontend_input'); + foreach ($frontendInput->getValues() as $frontendValue) { + if (in_array($frontendValue['value'], $this->supportedTypes, true)) { + $filteredValues[] = $frontendValue; + } + } + $frontendInput->setValues($filteredValues); + } + + /** + * @return bool + */ + private function isVariationsPopupUsed() + { + $popup = $this->request->getParam('popup'); + $productTab = $this->request->getParam('product_tab') === 'variations'; + return $popup && $productTab; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php new file mode 100644 index 0000000000000..a6f15699d417e --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php @@ -0,0 +1,174 @@ +objectManager = new ObjectManager($this); + } + + /** + * @return void + */ + public function testExecuteWhenBlockNotPassed() + { + $target = $this->createTarget($this->createRequestMock(false)); + $event = $this->createEventMock(); + $this->assertEquals(null, $target->execute($event)); + } + + /** + * @param RequestInterface|MockObject $request + * @param array $supportedTypes + * @return HideUnsupportedAttributeTypes + */ + private function createTarget(MockObject $request, array $supportedTypes = []) + { + return $this->objectManager->getObject( + HideUnsupportedAttributeTypes::class, + [ + 'request' => $request, + 'supportedTypes' => $supportedTypes + ] + ); + } + + private function createRequestMock($popup, $productTab = 'variations') + { + $request = $this->getMockBuilder(RequestInterface::class) + ->setMethods(['getParam']) + ->getMockForAbstractClass(); + $request->method('getParam') + ->willReturnCallback( + function ($name) use ($popup, $productTab) { + switch ($name) { + case 'popup': + return $popup; + case 'product_tab': + return $productTab; + default: + return null; + } + } + ); + return $request; + } + + /** + * @param MockObject|null $form + * @return EventObserver|MockObject + * @internal param null|MockObject $block + */ + private function createEventMock(MockObject $form = null) + { + $event = $this->getMockBuilder(EventObserver::class) + ->setMethods(['getForm', 'getBlock']) + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->any()) + ->method('getForm') + ->willReturn($form); + return $event; + } + + /** + * @dataProvider executeDataProvider + */ + public function testExecuteWithDefaultTypes(array $supportedTypes, array $originalValues, array $expectedValues) + { + $target = $this->createTarget($this->createRequestMock(true), $supportedTypes); + $event = $this->createEventMock($this->createForm($originalValues, $expectedValues)); + $this->assertEquals(null, $target->execute($event)); + } + + public function executeDataProvider() + { + return [ + 'testWithDefaultTypes' => [ + 'supportedTypes' => ['select'], + 'originalValues' => [ + $this->createFrontendInputValue('text2', 'Text2'), + $this->createFrontendInputValue('select', 'Select'), + $this->createFrontendInputValue('text', 'Text'), + $this->createFrontendInputValue('multiselect', 'Multiselect'), + $this->createFrontendInputValue('text3', 'Text3'), + ], + 'expectedValues' => [ + $this->createFrontendInputValue('select', 'Select'), + ], + ], + 'testWithCustomTypes' => [ + 'supportedTypes' => ['select', 'custom_type', 'second_custom_type'], + 'originalValues' => [ + $this->createFrontendInputValue('custom_type', 'CustomType'), + $this->createFrontendInputValue('text2', 'Text2'), + $this->createFrontendInputValue('select', 'Select'), + $this->createFrontendInputValue('text', 'Text'), + $this->createFrontendInputValue('second_custom_type', 'SecondCustomType'), + $this->createFrontendInputValue('multiselect', 'Multiselect'), + $this->createFrontendInputValue('text3', 'Text3'), + ], + 'expectedValues' => [ + $this->createFrontendInputValue('custom_type', 'CustomType'), + $this->createFrontendInputValue('select', 'Select'), + $this->createFrontendInputValue('second_custom_type', 'SecondCustomType'), + ], + ] + ]; + } + + private function createFrontendInputValue($value, $label) + { + return ['value' => $value, 'label' => $label]; + } + + private function createForm(array $originalValues = [], array $expectedValues = []) + { + $form = $this->getMockBuilder(\Magento\Framework\Data\Form::class) + ->setMethods(['getElement']) + ->disableOriginalConstructor() + ->getMock(); + $frontendInput = $this->getMockBuilder(\Magento\Framework\Data\Form\Element\Select::class) + ->setMethods(['getValues', 'setValues']) + ->disableOriginalConstructor() + ->getMock(); + $frontendInput->expects($this->once()) + ->method('getValues') + ->willReturn($originalValues); + $frontendInput->expects($this->once()) + ->method('setValues') + ->with($expectedValues) + ->willReturnSelf(); + $form->expects($this->once()) + ->method('getElement') + ->with('frontend_input') + ->willReturn($frontendInput); + return $form; + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/events.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/events.xml new file mode 100644 index 0000000000000..88df2d60a7dd4 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/events.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 992642ddc0871..c57194c664bd1 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -151,4 +151,11 @@ Magento\Framework\App\Cache\Type\Collection + + + + select + + + diff --git a/app/code/Magento/Downloadable/Model/Product/Type.php b/app/code/Magento/Downloadable/Model/Product/Type.php index 8b385cf74bd38..64c00d208b86f 100644 --- a/app/code/Magento/Downloadable/Model/Product/Type.php +++ b/app/code/Magento/Downloadable/Model/Product/Type.php @@ -164,10 +164,11 @@ public function getLinks($product) */ public function hasLinks($product) { - if ($product->hasData('links_exist')) { - return $product->getData('links_exist'); + $hasLinks = $product->getData('links_exist'); + if (null === $hasLinks) { + $hasLinks = (count($this->getLinks($product)) > 0); } - return count($this->getLinks($product)) > 0; + return $hasLinks; } /** diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeTest.php index 9ee0351e64d13..c4be8f5d5aee1 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeTest.php @@ -17,6 +17,7 @@ class TypeTest extends \PHPUnit_Framework_TestCase * @var \Magento\Downloadable\Model\Product\Type */ private $target; + /** * @var TypeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -83,6 +84,7 @@ protected function setUp() 'getDownloadableData', 'setTypeHasOptions', 'setLinksExist', + 'getDownloadableLinks', '__wakeup', ], [], @@ -144,4 +146,12 @@ public function testBeforeSave() { $this->target->beforeSave($this->product); } + + public function testHasLinks() + { + $this->product->expects($this->exactly(2)) + ->method('getDownloadableLinks') + ->willReturn(['link1', 'link2']); + $this->assertTrue($this->target->hasLinks($this->product)); + } } diff --git a/app/code/Magento/Eav/Model/ResourceModel/CreateHandler.php b/app/code/Magento/Eav/Model/ResourceModel/CreateHandler.php index c988700ee94b3..1050280a46300 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/CreateHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/CreateHandler.php @@ -14,6 +14,7 @@ /** * Class CreateHandler + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CreateHandler implements AttributeInterface { @@ -87,6 +88,7 @@ protected function getAttributes($entityType) * @return array * @throws \Exception * @throws \Magento\Framework\Exception\ConfigurationMismatchException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entityType, $entityData, $arguments = []) { @@ -100,6 +102,7 @@ public function execute($entityType, $entityData, $arguments = []) continue; } if (isset($entityData[$attribute->getAttributeCode()]) + && !is_array($entityData[$attribute->getAttributeCode()]) && !$attribute->isValueEmpty($entityData[$attribute->getAttributeCode()]) ) { $this->attributePersistor->registerInsert( diff --git a/app/code/Magento/Swatches/etc/di.xml b/app/code/Magento/Swatches/etc/di.xml index 26c86388231e9..525456d1f9eb6 100644 --- a/app/code/Magento/Swatches/etc/di.xml +++ b/app/code/Magento/Swatches/etc/di.xml @@ -21,6 +21,14 @@ + + + + swatch_visual + swatch_text + + + diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 3ebdc66edf3ba..555c10ee04b70 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -703,27 +703,21 @@ define([ ], "less-than-equals-to": [ function(value, params) { - if ($.isNumeric($(params).val()) && $.isNumeric(value)) { - this.lteToVal = $(params).val(); - return parseFloat(value) <= parseFloat($(params).val()); + if ($.isNumeric(params) && $.isNumeric(value)) { + return parseFloat(value) <= parseFloat(params); } return true; }, - function() { - return 'Please enter a value less than or equal to %s.'.replace('%s', this.lteToVal); - } + $.mage.__('Please enter a value less than or equal to {0}.') ], "greater-than-equals-to": [ function(value, params) { - if ($.isNumeric($(params).val()) && $.isNumeric(value)) { - this.gteToVal = $(params).val(); - return parseFloat(value) >= parseFloat($(params).val()); + if ($.isNumeric(params) && $.isNumeric(value)) { + return parseFloat(value) >= parseFloat(params); } return true; }, - function() { - return 'Please enter a value greater than or equal to %s.'.replace('%s', this.gteToVal); - } + $.mage.__('Please enter a value greater than or equal to {0}.') ], "validate-emails": [ function(value) { diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml index b9e5f6e317e62..6fcef33132603 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml @@ -195,8 +195,8 @@ Yes dynamic-8 20 - m/d/Y -1 day - m/d/Y +3 days + M j, Y -1 day + M j, Y +3 days default_dynamic catalogProductSimple::product_100_dollar,catalogProductSimple::product_40_dollar bundle_default diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Edit/Section/Variations/Config/Matrix.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Edit/Section/Variations/Config/Matrix.php index ed8cbc800b2e6..893d01f7ccb92 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Edit/Section/Variations/Config/Matrix.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Edit/Section/Variations/Config/Matrix.php @@ -79,7 +79,7 @@ class Matrix extends Form * * @var string */ - protected $deleteVariation = '[data-bind*="deleteRecord"]'; + protected $deleteVariation = '[data-bind*="Remove Product"]'; /** * Choose a different Product button selector. @@ -193,8 +193,14 @@ public function getTemplateBlock() public function deleteVariations() { - $variations = $this->_rootElement->getElements($this->variationRow, Locator::SELECTOR_XPATH); - foreach (array_reverse($variations) as $variation) { + $rowLocator = sprintf($this->variationRowByNumber, 1); + $variationText = ''; + while ($this->_rootElement->find($rowLocator, Locator::SELECTOR_XPATH)->isVisible()) { + $variation = $this->_rootElement->find($rowLocator, Locator::SELECTOR_XPATH); + if ($variationText == $variation->getText()) { + throw new \Exception("Failed to delete configurable product variation"); + } + $variationText = $variation->getText(); $variation->find($this->actionMenu)->hover(); $variation->find($this->actionMenu)->click(); $variation->find($this->deleteVariation)->click(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php index 6390f1d3c995c..fda3851989ea8 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php @@ -364,6 +364,7 @@ public function advancedSearchDataProvider() } /** + * @magentoDataFixture Magento/Framework/Search/_files/filterable_attribute.php * @magentoConfigFixture current_store catalog/search/engine mysql */ public function testCustomFilterableAttribute() @@ -384,12 +385,46 @@ public function testCustomFilterableAttribute() $this->requestBuilder->bind('select_attribute', $selectOptions->getLastItem()->getId()); $this->requestBuilder->bind('multiselect_attribute', $multiselectOptions->getLastItem()->getId()); - $this->requestBuilder->bind('price.from', 9); - $this->requestBuilder->bind('price.to', 12); + $this->requestBuilder->bind('price.from', 98); + $this->requestBuilder->bind('price.to', 100); $this->requestBuilder->bind('category_ids', 2); $this->requestBuilder->setRequestName('filterable_custom_attributes'); $queryResponse = $this->executeQuery(); $this->assertEquals(1, $queryResponse->count()); } + + /** + * Advanced search request using date product attribute + * + * @param $rangeFilter + * @param $expectedRecordsCount + * @magentoDataFixture Magento/Framework/Search/_files/date_attribute.php + * @magentoConfigFixture current_store catalog/search/engine mysql + * @dataProvider dateDataProvider + */ + public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount) + { + array_walk($rangeFilter, function (&$item) { + if (!empty($item)) { + $item = gmdate('c', strtotime($item)) . 'Z'; + } + }); + $this->requestBuilder->bind('date.from', $rangeFilter['from']); + $this->requestBuilder->bind('date.to', $rangeFilter['to']); + $this->requestBuilder->setRequestName('advanced_search_date_field'); + + $queryResponse = $this->executeQuery(); + $this->assertEquals($expectedRecordsCount, $queryResponse->count()); + } + + public function dateDataProvider() + { + return [ + [['from' => '2000-01-01', 'to' => '2000-01-01'], 1], //Y-m-d + [['from' => '2000-01-01', 'to' => ''], 1], + [['from' => '1999-12-31', 'to' => '2000-01-01'], 1], + [['from' => '2000-02-01', 'to' => ''], 0], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute.php new file mode 100644 index 0000000000000..606a53acbfd72 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute.php @@ -0,0 +1,59 @@ +create( + 'Magento\Catalog\Setup\CategorySetup', + ['resourceName' => 'catalog_setup'] +); + +/** @var $selectAttribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$dateAttribute = $objectManager->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$dateAttribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_filterable' => 1, + 'backend_type' => 'datetime', + 'frontend_input' => 'date', + 'frontend_label' => 'Test Date', + ] +); +$dateAttribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $dateAttribute->getId()); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create('Magento\Catalog\Model\Product'); +$product + ->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('Simple Product with date attribute') + ->setSku('simple_product_with_date_attribute') + ->setPrice(1) + ->setCategoryIds([2]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1]) + ->save(); + +$objectManager->get('Magento\Catalog\Model\Product\Action') + ->updateAttributes( + [$product->getId()], + [ + $dateAttribute->getAttributeCode() => '01/01/2000' // m/d/Y + ], + $product->getStoreId() + ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute_rollback.php new file mode 100644 index 0000000000000..2f59c100ca3b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/date_attribute_rollback.php @@ -0,0 +1,35 @@ +create( + 'Magento\Catalog\Setup\CategorySetup', + ['resourceName' => 'catalog_setup'] +); +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); +$product = $product->loadByAttribute('sku', 'simple_product_with_date_attribute'); +if ($product->getId()) { + $product->delete(); +} + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'date_attribute'); +if ($attribute->getId()) { + $attribute->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attribute.php index 3103d25cc1fb5..6a2e967560987 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attribute.php @@ -81,7 +81,7 @@ )->setSku( 'simple_product_' . $option->getId() )->setPrice( - 10 + 99 )->setCategoryIds( [2] )->setVisibility( diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml index 76e044afcba49..0cdfa11c3b6b5 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml @@ -366,4 +366,25 @@ 0 10 + + + + + + + + + + + + + + + + + + + 0 + 10 + diff --git a/lib/internal/Magento/Framework/Search/Request/Cleaner.php b/lib/internal/Magento/Framework/Search/Request/Cleaner.php index 7d2ad5173fd6b..bceeedc79df9a 100644 --- a/lib/internal/Magento/Framework/Search/Request/Cleaner.php +++ b/lib/internal/Magento/Framework/Search/Request/Cleaner.php @@ -57,7 +57,7 @@ public function clean(array $requestData) $this->clear(); if (empty($requestData['queries']) && empty($requestData['filters'])) { - throw new EmptyRequestDataException(new Phrase('Request query and filter is not set')); + throw new EmptyRequestDataException(new Phrase('Request query and filters are not set')); } return $requestData; diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Request/CleanerTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Request/CleanerTest.php index 2d3368345b910..41346379f1ebc 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Request/CleanerTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Request/CleanerTest.php @@ -338,7 +338,7 @@ public function testCleanQueryNotExists() /** * @expectedException \Magento\Framework\Search\Request\EmptyRequestDataException - * @expectedExceptionMessage Request query and filter is not set + * @expectedExceptionMessage Request query and filters are not set */ public function testCleanEmptyQueryAndFilter() {