From dfb264b5455fa1247c623d9d8321f781aece34af Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Thu, 6 Apr 2017 17:38:33 +0300 Subject: [PATCH 001/363] MAGETWO-66278: Timeline fix. --- app/code/Magento/Ui/view/base/web/js/timeline/timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/timeline/timeline.js b/app/code/Magento/Ui/view/base/web/js/timeline/timeline.js index 571b03317ac09..e86064a8606f8 100644 --- a/app/code/Magento/Ui/view/base/web/js/timeline/timeline.js +++ b/app/code/Magento/Ui/view/base/web/js/timeline/timeline.js @@ -104,7 +104,7 @@ define([ * @returns {Boolean} */ isActive: function (record) { - return record.status === 1; + return Number(record.status) === 1; }, /** @@ -115,7 +115,7 @@ define([ * @returns {Boolean} */ isUpcoming: function (record) { - return record.status === 2; + return Number(record.status) === 2; }, /** From f19cd09708b92b1efec9f021748b0a50ef5c0e8d Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Fri, 7 Apr 2017 14:50:01 +0300 Subject: [PATCH 002/363] MAGETWO-61097: [BACKPORT] UK mobile phone number validation is outdated --- app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js | 2 +- lib/web/mage/validation.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 4702120860285..c78f253c8aec0 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 @@ -213,7 +213,7 @@ define([ ], "mobileUK": [ function(value) { - return value.length > 9 && value.match(/^((0|\+44)7(5|6|7|8|9){1}\d{2}\s?\d{6})$/); + return value.length > 9 && value.match(/^((0|\+44)7\d{3}\s?\d{6})$/); }, $.mage.__('Please specify a valid mobile number') ], diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 0238f2cb9840b..9d57001b6d2dc 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -320,7 +320,7 @@ "mobileUK": [ function (phone_number, element) { return this.optional(element) || phone_number.length > 9 && - phone_number.match(/^((0|\+44)7(5|6|7|8|9){1}\d{2}\s?\d{6})$/); + phone_number.match(/^((0|\+44)7\d{3}\s?\d{6})$/); }, 'Please specify a valid mobile number' ], From 96e08fad04e36ea1415eb55cf50b62d3af57ca96 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 11:33:21 +0300 Subject: [PATCH 003/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../Adminhtml/Promo/Quote/Generate.php | 69 +++++++-- .../Model/Service/CouponManagementService.php | 2 - .../Block/Onepage/Payment/DiscountCodes.php | 13 +- .../Promo/Quote/Edit/PromoQuoteForm.xml | 16 +++ .../BlockPromoSalesRuleEditTabCoupons.php | 97 +++++++++++++ .../Grid.php | 37 +++++ ...ssertCouponCodeSuccessGeneratedMessage.php | 61 ++++++++ .../Constraint/AssertUsesPerCouponWorks.php | 135 ++++++++++++++++++ .../SalesRule/Test/Fixture/SalesRule.xml | 6 + .../TestCase/CreateSalesRuleEntityTest.php | 33 ++++- .../TestCase/CreateSalesRuleEntityTest.xml | 23 +++ 11 files changed, 475 insertions(+), 17 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php index 4db27d3607c9f..0771f826b33e6 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php @@ -8,6 +8,39 @@ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote { + /** + * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory + */ + private $generationSpecFactory; + /** + * @var \Magento\SalesRule\Model\Service\CouponManagementService + */ + private $couponManagementService; + + /** + * Generate constructor. + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\Registry $coreRegistry + * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory + * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter + * @param \Magento\SalesRule\Model\Service\CouponManagementService|null $couponManagementService + * @param \Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory|null $generationSpecFactory + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\Registry $coreRegistry, + \Magento\Framework\App\Response\Http\FileFactory $fileFactory, + \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, + \Magento\SalesRule\Model\Service\CouponManagementService $couponManagementService = null, + \Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory $generationSpecFactory = null + ) { + parent::__construct($context, $coreRegistry, $fileFactory, $dateFilter); + $this->generationSpecFactory = $generationSpecFactory ?: + $this->_objectManager->get(\Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory::class); + $this->couponManagementService = $couponManagementService ?: + $this->_objectManager->get(\Magento\SalesRule\Model\Service\CouponManagementService::class); + } + /** * Generate Coupons action * @@ -17,6 +50,7 @@ public function execute() { if (!$this->getRequest()->isAjax()) { $this->_forward('noroute'); + return; } $result = []; @@ -35,18 +69,15 @@ public function execute() $data = $inputFilter->getUnescaped(); } - /** @var $generator \Magento\SalesRule\Model\Coupon\Massgenerator */ - $generator = $this->_objectManager->get('Magento\SalesRule\Model\Coupon\Massgenerator'); - if (!$generator->validateData($data)) { - $result['error'] = __('Invalid data provided'); - } else { - $generator->setData($data); - $generator->generatePool(); - $generated = $generator->getGeneratedCount(); - $this->messageManager->addSuccess(__('%1 coupon(s) have been generated.', $generated)); - $this->_view->getLayout()->initMessages(); - $result['messages'] = $this->_view->getLayout()->getMessagesBlock()->getGroupedHtml(); - } + $data = $this->convertCouponSpecData($data); + $couponSpec = $this->generationSpecFactory->create(['data' => $data]); + $couponCodes = $this->couponManagementService->generate($couponSpec); + $generated = count($couponCodes); + $this->messageManager->addSuccess(__('%1 coupon(s) have been generated.', $generated)); + $this->_view->getLayout()->initMessages(); + $result['messages'] = $this->_view->getLayout()->getMessagesBlock()->getGroupedHtml(); + } catch (\Magento\Framework\Exception\InputException $inputException) { + $result['error'] = __('Invalid data provided'); } catch (\Magento\Framework\Exception\LocalizedException $e) { $result['error'] = $e->getMessage(); } catch (\Exception $e) { @@ -60,4 +91,18 @@ public function execute() $this->_objectManager->get('Magento\Framework\Json\Helper\Data')->jsonEncode($result) ); } + + /** + * We should map old values to new one + * We need to do this, as new service with another key names was added + * + * @param array $data + * @return array + */ + private function convertCouponSpecData(array $data) + { + $data['quantity'] = $data['qty']; + + return $data; + } } diff --git a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php index 153b26fbad496..a69e17478dea4 100644 --- a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php +++ b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php @@ -5,8 +5,6 @@ */ namespace Magento\SalesRule\Model\Service; -use Magento\SalesRule\Model\Coupon; - /** * Coupon management service class * diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php index bfc9988dfd060..a6fc1f3a04192 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php @@ -15,6 +15,15 @@ */ class DiscountCodes extends Form { + // @codingStandardsIgnoreStart + /** + * Message after coupon applying. + * + * @var string + */ + protected $couponApplyingMessage = './/div[contains(@class,"messages")]//div[contains(@class,"message")]'; + // @codingStandardsIgnoreEnd + /** * Form wrapper selector * @@ -47,12 +56,14 @@ class DiscountCodes extends Form * Enter discount code and click apply button * * @param string $code - * @return void + * @return string */ public function applyCouponCode($code) { $this->_rootElement->find($this->openForm, Locator::SELECTOR_CSS)->click(); $this->_rootElement->find($this->couponCode, Locator::SELECTOR_CSS)->setValue($code); $this->_rootElement->find($this->applyButton, Locator::SELECTOR_CSS)->click(); + + return $this->_rootElement->find($this->couponApplyingMessage, Locator::SELECTOR_XPATH)->getText(); } } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.xml index 7e30946214475..7e214aa38acf5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.xml @@ -27,6 +27,9 @@ select + + checkbox + @@ -80,4 +83,17 @@ [data-index="labels"] css selector + + \Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section\BlockPromoSalesRuleEditTabCoupons + [data-index="block_promo_sales_rule_edit_tab_coupons"] + css selector + + + + + + + + + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php new file mode 100644 index 0000000000000..b10da4ab4cf6c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php @@ -0,0 +1,97 @@ +_rootElement->find($this->generateButtonSelector, Locator::SELECTOR_XPATH)->click(); + } + + /** + * Get success message from section. + * + * @return string + */ + public function getSuccessMessage() + { + $this->waitForElementVisible($this->successMessage); + + return $this->_rootElement->find($this->successMessage)->getText(); + } + + /** + * Get coupon codes grid. + * + * @return \Magento\Mtf\Block\BlockInterface + */ + public function getCouponGrid() + { + $element = $this->_rootElement->find($this->gridSelector); + + return $this->blockFactory->create(Grid::class, ['element' => $element]); + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php new file mode 100644 index 0000000000000..572acbe6b8149 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php @@ -0,0 +1,37 @@ +waitLoader(); + + $data = []; + $rows = $this->_rootElement->getElements($this->rowItem); + foreach ($rows as $row) { + $rowData = []; + foreach ($columns as $columnName) { + $rowData[$columnName] = trim($row->find('.col-' . $columnName)->getText()); + } + + $data[] = $rowData; + } + + return $data; + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php new file mode 100644 index 0000000000000..8c2a1db140659 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php @@ -0,0 +1,61 @@ +getQty()); + + /** @var PromoQuoteForm $salesRuleForm */ + $salesRuleForm = $promoQuoteNew->getSalesRuleForm(); + + /** @var BlockPromoSalesRuleEditTabCoupons $manageCouponCodesSection */ + $manageCouponCodesSection = $salesRuleForm->getSection('block_promo_sales_rule_edit_tab_coupons'); + + $actualMessage = $manageCouponCodesSection->getSuccessMessage(); + + \PHPUnit_Framework_Assert::assertEquals( + $expectedMessage, + $actualMessage, + 'Wrong success message is displayed.' + . "\nExpected: " . $expectedMessage + . "\nActual: " . $actualMessage + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Coupon generating success message is present.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php new file mode 100644 index 0000000000000..6572ff4a36eba --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php @@ -0,0 +1,135 @@ +getUsesPerCoupon() + 1; $i++) { + //add product to cart. + $objectManager->create( + \Magento\Checkout\Test\TestStep\AddProductsToTheCartStep::class, + ['products' => [$productForSalesRule1]] + )->run(); + + // go to checkout. + $checkoutOnepage->open(); + + //fill shipping address. + $objectManager->create( + \Magento\Checkout\Test\TestStep\FillShippingAddressStep::class, + ['shippingAddress' => $shippingAddress] + )->run(); + + //fill sipping method. + $objectManager->create( + \Magento\Checkout\Test\TestStep\FillShippingMethodStep::class, + ['shipping' => $shipping] + )->run(); + + // select payment method. + $objectManager->create( + \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, + ['payment' => $payment]) + ->run(); + + // apply coupon code and get message. + $message = $checkoutOnepage->getDiscountCodesBlock()->applyCouponCode($generatedCouponCodes[0]['code']); + + // check coupon code applying message. + $this->assertCouponCodeApplyingMessage($message, $salesRule->getUsesPerCoupon(), $i); + + // place order. + $objectManager->create(\Magento\Checkout\Test\TestStep\PlaceOrderStep::class)->run(); + } + } + + /** + * @param string $message + * @param int $usesPerCoupon + * @param int $i + * + * @return void + */ + private function assertCouponCodeApplyingMessage($message, $usesPerCoupon, $i) + { + if ($usesPerCoupon > $i) { + \PHPUnit_Framework_Assert::assertEquals( + $this->successCouponAppliedMessage, + $message + ); + } else { + \PHPUnit_Framework_Assert::assertEquals( + $this->errorCouponAppliedMessage, + $message + ); + } + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Uses per coupon configuration works ok.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml index 03e4d31406e2a..dd652d1f646fb 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml @@ -43,5 +43,11 @@ + + + + + + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index e19b79e3474e8..6a4f9012a3fad 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Test\TestCase; +use Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\PromoQuoteForm; +use Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section\BlockPromoSalesRuleEditTabCoupons; +use Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section\BlockPromoSalesRuleEditTabCoupons\Grid; use Magento\SalesRule\Test\Fixture\SalesRule; use Magento\SalesRule\Test\Page\Adminhtml\PromoQuoteEdit; use Magento\SalesRule\Test\Page\Adminhtml\PromoQuoteIndex; @@ -102,15 +105,20 @@ public function __inject( * @param CatalogProductSimple $productForSalesRule2 * @param Customer $customer * @param string $conditionEntity + * @param SalesRule $manageCouponCodes + * + * @return array */ public function testCreateSalesRule( SalesRule $salesRule, CatalogProductSimple $productForSalesRule1, CatalogProductSimple $productForSalesRule2 = null, Customer $customer = null, - $conditionEntity = null + $conditionEntity = null, + SalesRule $manageCouponCodes = null ) { $replace = null; + $generatedCouponCodes = []; $this->salesRuleName = $salesRule->getName(); // Prepare data @@ -128,7 +136,28 @@ public function testCreateSalesRule( // Steps $this->promoQuoteNew->open(); $this->promoQuoteNew->getSalesRuleForm()->fill($salesRule, null, $replace); - $this->promoQuoteNew->getFormPageActions()->save(); + + if ($salesRule->getUseAutoGeneration() == 'Yes') { + $this->promoQuoteNew->getFormPageActions()->saveAndContinue(); + + /** @var PromoQuoteForm $salesRuleForm */ + $salesRuleForm = $this->promoQuoteNew->getSalesRuleForm(); + $salesRuleForm->fill($manageCouponCodes); + + /** @var BlockPromoSalesRuleEditTabCoupons $manageCouponCodesSection */ + $manageCouponCodesSection = $salesRuleForm->getSection('block_promo_sales_rule_edit_tab_coupons'); + $manageCouponCodesSection->pressGenerateButton(); + + /** @var Grid $couponGrid */ + $couponGrid = $manageCouponCodesSection->getCouponGrid(); + + $generatedCouponCodes = $couponGrid->getRowsData(['code']);; + ; + } else { + $this->promoQuoteNew->getFormPageActions()->save(); + } + + return ['generatedCouponCodes' => $generatedCouponCodes]; } /** diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 2fed474f73d6a..f9713a9a3960e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -403,6 +403,29 @@ 75.00 + --> + + Cart Price Rule3 %isolation% + Cart Price Rule Description %isolation% + Yes + Main Website + NOT LOGGED IN + Specific Coupon + Yes + Fixed amount discount for whole cart + 1 + 50 + No + No + 1 + simple_for_salesrule_1 + 5 + US_address_1 + checkmo + Flat Rate + Fixed + + From 2075175821070ed544e8bdb2610b65b53b29a33d Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 11:33:40 +0300 Subject: [PATCH 004/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index f9713a9a3960e..ece5e09c55493 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -403,7 +403,7 @@ 75.00 - --> + Cart Price Rule3 %isolation% Cart Price Rule Description %isolation% From fd0a6945f1974ce81f7472323598beff59b63809 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 11:40:11 +0300 Subject: [PATCH 005/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../Test/Constraint/AssertUsesPerCouponWorks.php | 13 +++++++------ .../TestSuite/InjectableTests/MAGETWO-67267.xml | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php index 6572ff4a36eba..01016bb8bb136 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php @@ -84,18 +84,19 @@ public function processAssert( ['shipping' => $shipping] )->run(); - // select payment method. - $objectManager->create( - \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, - ['payment' => $payment]) - ->run(); - // apply coupon code and get message. $message = $checkoutOnepage->getDiscountCodesBlock()->applyCouponCode($generatedCouponCodes[0]['code']); // check coupon code applying message. $this->assertCouponCodeApplyingMessage($message, $salesRule->getUsesPerCoupon(), $i); + + // select payment method. + $objectManager->create( + \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, + ['payment' => $payment]) + ->run(); + // place order. $objectManager->create(\Magento\Checkout\Test\TestStep\PlaceOrderStep::class)->run(); } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml new file mode 100644 index 0000000000000..eafb758b22576 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 68513b643405e2a58e2efac39277645f5053c39e Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 12:29:15 +0300 Subject: [PATCH 006/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php | 1 + .../SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php | 1 - .../Magento/Test/Integrity/_files/blacklist/reference.txt | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php index 0771f826b33e6..d8a6845f7a019 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php @@ -12,6 +12,7 @@ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory */ private $generationSpecFactory; + /** * @var \Magento\SalesRule\Model\Service\CouponManagementService */ diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php index 01016bb8bb136..3fa5ad3e9601a 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php @@ -90,7 +90,6 @@ public function processAssert( // check coupon code applying message. $this->assertCouponCodeApplyingMessage($message, $salesRule->getUsesPerCoupon(), $i); - // select payment method. $objectManager->create( \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt index cafac52cd5afe..7bb9bd2cfaff1 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt @@ -59,7 +59,9 @@ Model3 \Magento\TestModule2\Service\V1\Entity\ItemBuilder \Magento\TestModule4\Service\V1\Entity\DataObjectResponse \Magento\TestModule4\Service\V1\Entity\DataObjectRequest +\Magento\Mtf\ObjectManager \Magento\Mtf\ObjectManager\Config\Mapper\Dom +\Magento\Mtf\Block\BlockInterface \Magento\Mtf\Data\Argument\Interpreter\Constant \Magento\Mtf\Data\Argument\Interpreter\Boolean \Magento\Mtf\Data\Argument\Interpreter\StringType From aee5c61be7fafcebb83b287a56c8692cfb109e19 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 13:10:21 +0300 Subject: [PATCH 007/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php | 4 ++-- .../SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php | 3 +-- .../InjectableTests/{MAGETWO-67267.xml => MAGETWO-65161.xml} | 0 3 files changed, 3 insertions(+), 4 deletions(-) rename dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/{MAGETWO-67267.xml => MAGETWO-65161.xml} (100%) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php index 3fa5ad3e9601a..86ade02802ed1 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php @@ -93,8 +93,8 @@ public function processAssert( // select payment method. $objectManager->create( \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, - ['payment' => $payment]) - ->run(); + ['payment' => $payment] + )->run(); // place order. $objectManager->create(\Magento\Checkout\Test\TestStep\PlaceOrderStep::class)->run(); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index 6a4f9012a3fad..a15a1dc7f4e4e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -151,8 +151,7 @@ public function testCreateSalesRule( /** @var Grid $couponGrid */ $couponGrid = $manageCouponCodesSection->getCouponGrid(); - $generatedCouponCodes = $couponGrid->getRowsData(['code']);; - ; + $generatedCouponCodes = $couponGrid->getRowsData(['code']); } else { $this->promoQuoteNew->getFormPageActions()->save(); } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml similarity index 100% rename from dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-67267.xml rename to dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml From c43eba1cf3742bd916ac2c1d615d6d9825f6af51 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Mon, 10 Apr 2017 15:17:38 +0300 Subject: [PATCH 008/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-65161.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml deleted file mode 100644 index eafb758b22576..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 7a41d4dd4da5a4b9e37b52bc150fb32cb684d370 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Tue, 11 Apr 2017 11:28:34 +0300 Subject: [PATCH 009/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../Block/Onepage/Payment/DiscountCodes.php | 25 +++++++------ .../Promo/Quote/Edit/PromoQuoteForm.php | 19 ++++++++++ .../BlockPromoSalesRuleEditTabCoupons.php | 36 +++++-------------- .../Grid.php | 22 ------------ ...ssertCouponCodeSuccessGeneratedMessage.php | 8 +++-- ...ouponWorks.php => AssertUsesPerCoupon.php} | 34 ++++++++---------- .../TestCase/CreateSalesRuleEntityTest.php | 20 +++++++---- .../TestCase/CreateSalesRuleEntityTest.xml | 5 ++- .../InjectableTests/MAGETWO-65161.xml | 15 ++++++++ .../Integrity/_files/blacklist/reference.txt | 2 -- 10 files changed, 92 insertions(+), 94 deletions(-) rename dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/{AssertUsesPerCouponWorks.php => AssertUsesPerCoupon.php} (83%) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php index a6fc1f3a04192..0b7148538d5c5 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php @@ -10,59 +10,64 @@ use Magento\Mtf\Client\Locator; /** - * Class DiscountCodes - * Discount codes block + * Discount codes block. */ class DiscountCodes extends Form { - // @codingStandardsIgnoreStart /** * Message after coupon applying. * * @var string */ protected $couponApplyingMessage = './/div[contains(@class,"messages")]//div[contains(@class,"message")]'; - // @codingStandardsIgnoreEnd /** - * Form wrapper selector + * Form wrapper selector. * * @var string */ protected $formWrapper = '.content'; /** - * Open discount codes form selector + * Open discount codes form selector. * * @var string */ protected $openForm = '.payment-option-title'; /** - * Fill discount code input selector + * Fill discount code input selector. * * @var string */ protected $couponCode = '#discount-code'; /** - * Click apply button selector + * Click apply button selector. * * @var string */ protected $applyButton = '.action.action-apply'; /** - * Enter discount code and click apply button + * Enter discount code and click apply button. * * @param string $code - * @return string + * @return void */ public function applyCouponCode($code) { $this->_rootElement->find($this->openForm, Locator::SELECTOR_CSS)->click(); $this->_rootElement->find($this->couponCode, Locator::SELECTOR_CSS)->setValue($code); $this->_rootElement->find($this->applyButton, Locator::SELECTOR_CSS)->click(); + } + + /** + * Get message after coupon applying. + * + * @return string + */ + public function getCouponApplyingMessage() { return $this->_rootElement->find($this->couponApplyingMessage, Locator::SELECTOR_XPATH)->getText(); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php index 607dc20047dd5..7882bd9b6edd0 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php @@ -8,6 +8,7 @@ use Magento\Ui\Test\Block\Adminhtml\FormSections; use Magento\Mtf\Client\Element\SimpleElement; use Magento\Mtf\Fixture\FixtureInterface; +use Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section\BlockPromoSalesRuleEditTabCoupons; /** * Sales rule edit form. @@ -45,6 +46,24 @@ public function fill(FixtureInterface $fixture, SimpleElement $element = null, a $this->fillContainers($sections, $element); } + /** + * Generate coupons for Cart Rule. + * + * @param array $generateSettings + * + * @return void + */ + public function generateCoupons(array $generateSettings) + { + $this->fillContainers([ + 'block_promo_sales_rule_edit_tab_coupons' => $generateSettings + ]); + + /** @var BlockPromoSalesRuleEditTabCoupons $couponSection */ + $couponSection = $this->getSection('block_promo_sales_rule_edit_tab_coupons'); + $couponSection->pressGenerateButton(); + } + /** * Replace placeholders in each values of data. * diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php index b10da4ab4cf6c..715b44f39c1ef 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons.php @@ -6,12 +6,7 @@ namespace Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section; -use Magento\Mtf\Block\BlockFactory; -use Magento\Mtf\Block\Mapper; -use Magento\Mtf\Client\BrowserInterface; -use Magento\Mtf\Client\Element\SimpleElement; use Magento\Mtf\Client\Locator; -use Magento\Mtf\Util\ModuleResolver\SequenceSorterInterface; use Magento\SalesRule\Test\Block\Adminhtml\Promo\Quote\Edit\Section\BlockPromoSalesRuleEditTabCoupons\Grid; use Magento\Ui\Test\Block\Adminhtml\Section; @@ -39,27 +34,7 @@ class BlockPromoSalesRuleEditTabCoupons extends Section * * @var string */ - private $gridSelector = '#couponCodesGrid_table'; - - /** - * BlockPromoSalesRuleEditTabCoupons constructor. - * @param SimpleElement $element - * @param BlockFactory $blockFactory - * @param Mapper $mapper - * @param BrowserInterface $browser - * @param SequenceSorterInterface $sequenceSorter - * @param array $config - */ - public function __construct( - SimpleElement $element, - BlockFactory $blockFactory, - Mapper $mapper, - BrowserInterface $browser, - SequenceSorterInterface $sequenceSorter, - array $config = [] - ) { - parent::__construct($element, $blockFactory, $mapper, $browser, $sequenceSorter, $config); - } + private $gridSelector = '#couponCodesGrid'; /** * Press generate button to generate coupons. @@ -69,6 +44,8 @@ public function __construct( public function pressGenerateButton() { $this->_rootElement->find($this->generateButtonSelector, Locator::SELECTOR_XPATH)->click(); + + $this->waitForElementVisible($this->successMessage); } /** @@ -86,12 +63,15 @@ public function getSuccessMessage() /** * Get coupon codes grid. * - * @return \Magento\Mtf\Block\BlockInterface + * @return Grid */ public function getCouponGrid() { $element = $this->_rootElement->find($this->gridSelector); - return $this->blockFactory->create(Grid::class, ['element' => $element]); + /** @var Grid $couponGrid */ + $couponGrid = $this->blockFactory->create(Grid::class, ['element' => $element]); + + return $couponGrid; } } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php index 572acbe6b8149..36ffcbb3874a9 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php @@ -11,27 +11,5 @@ */ class Grid extends \Magento\Backend\Test\Block\Widget\Grid { - /** - * Get rows data - * - * @param array $columns - * @return array - */ - public function getRowsData(array $columns) - { - $this->waitLoader(); - $data = []; - $rows = $this->_rootElement->getElements($this->rowItem); - foreach ($rows as $row) { - $rowData = []; - foreach ($columns as $columnName) { - $rowData[$columnName] = trim($row->find('.col-' . $columnName)->getText()); - } - - $data[] = $rowData; - } - - return $data; - } } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php index 8c2a1db140659..493205f6e4ed5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCouponCodeSuccessGeneratedMessage.php @@ -23,14 +23,16 @@ class AssertCouponCodeSuccessGeneratedMessage extends AbstractConstraint * Assert that success message is displayed after generating coupon('s). * * @param PromoQuoteNew $promoQuoteNew - * @param SalesRule $manageCouponCodes + * @param array $generateCouponsSettings * @return void */ public function processAssert( PromoQuoteNew $promoQuoteNew, - SalesRule $manageCouponCodes + array $generateCouponsSettings ) { - $expectedMessage = sprintf(self::SUCCESS_MESSAGE, $manageCouponCodes->getQty()); + $qty = isset($generateCouponsSettings['qty']) ? $generateCouponsSettings['qty'] : null; + + $expectedMessage = sprintf(self::SUCCESS_MESSAGE, $qty); /** @var PromoQuoteForm $salesRuleForm */ $salesRuleForm = $promoQuoteNew->getSalesRuleForm(); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCoupon.php similarity index 83% rename from dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php rename to dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCoupon.php index 86ade02802ed1..ad487d63d91b7 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCouponWorks.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertUsesPerCoupon.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Test\Constraint; use Magento\Mtf\Constraint\AbstractConstraint; +use Magento\Mtf\TestStep\TestStepFactory; use Magento\SalesRule\Test\Fixture\SalesRule; use Magento\Checkout\Test\Page\CheckoutOnepage; use Magento\Catalog\Test\Fixture\CatalogProductSimple; @@ -14,7 +15,7 @@ /** * Assert uses per coupon configuration works ok. */ -class AssertUsesPerCouponWorks extends AbstractConstraint +class AssertUsesPerCoupon extends AbstractConstraint { /** * Message when coupon is applied successfully. @@ -30,13 +31,6 @@ class AssertUsesPerCouponWorks extends AbstractConstraint */ private $errorCouponAppliedMessage = 'Coupon code is not valid'; - /** - * First product from precondition. - * - * @var CatalogProductSimple - */ - protected $productForSalesRule1; - /** * Assert uses per coupon configuration works ok. * @@ -44,9 +38,10 @@ class AssertUsesPerCouponWorks extends AbstractConstraint * @param CatalogProductSimple $productForSalesRule1 * @param CheckoutOnepage $checkoutOnepage * @param array $shippingAddress - * @param array $generatedCouponCodes + * @param array $couponCodes * @param array $payment * @param array $shipping + * @param TestStepFactory $testStepFactory * * @return void */ @@ -55,16 +50,15 @@ public function processAssert( CatalogProductSimple $productForSalesRule1, CheckoutOnepage $checkoutOnepage, array $shippingAddress, - array $generatedCouponCodes, + array $couponCodes, array $payment, - array $shipping + array $shipping, + TestStepFactory $testStepFactory ) { - $objectManager = \Magento\Mtf\ObjectManager::getInstance(); - //need to place order one more time than uses_per_coupon to get error message. for ($i = 0; $i < $salesRule->getUsesPerCoupon() + 1; $i++) { //add product to cart. - $objectManager->create( + $testStepFactory->create( \Magento\Checkout\Test\TestStep\AddProductsToTheCartStep::class, ['products' => [$productForSalesRule1]] )->run(); @@ -73,31 +67,33 @@ public function processAssert( $checkoutOnepage->open(); //fill shipping address. - $objectManager->create( + $testStepFactory->create( \Magento\Checkout\Test\TestStep\FillShippingAddressStep::class, ['shippingAddress' => $shippingAddress] )->run(); //fill sipping method. - $objectManager->create( + $testStepFactory->create( \Magento\Checkout\Test\TestStep\FillShippingMethodStep::class, ['shipping' => $shipping] )->run(); // apply coupon code and get message. - $message = $checkoutOnepage->getDiscountCodesBlock()->applyCouponCode($generatedCouponCodes[0]['code']); + $checkoutOnepage->getDiscountCodesBlock()->applyCouponCode($couponCodes[0]); + + $message = $checkoutOnepage->getDiscountCodesBlock()->getCouponApplyingMessage(); // check coupon code applying message. $this->assertCouponCodeApplyingMessage($message, $salesRule->getUsesPerCoupon(), $i); // select payment method. - $objectManager->create( + $testStepFactory->create( \Magento\Checkout\Test\TestStep\SelectPaymentMethodStep::class, ['payment' => $payment] )->run(); // place order. - $objectManager->create(\Magento\Checkout\Test\TestStep\PlaceOrderStep::class)->run(); + $testStepFactory->create(\Magento\Checkout\Test\TestStep\PlaceOrderStep::class)->run(); } } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index a15a1dc7f4e4e..005888a358a00 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -105,7 +105,7 @@ public function __inject( * @param CatalogProductSimple $productForSalesRule2 * @param Customer $customer * @param string $conditionEntity - * @param SalesRule $manageCouponCodes + * @param array $generateCouponsSettings * * @return array */ @@ -115,10 +115,10 @@ public function testCreateSalesRule( CatalogProductSimple $productForSalesRule2 = null, Customer $customer = null, $conditionEntity = null, - SalesRule $manageCouponCodes = null + array $generateCouponsSettings = null ) { $replace = null; - $generatedCouponCodes = []; + $generatedCouponCodes = null; $this->salesRuleName = $salesRule->getName(); // Prepare data @@ -137,26 +137,32 @@ public function testCreateSalesRule( $this->promoQuoteNew->open(); $this->promoQuoteNew->getSalesRuleForm()->fill($salesRule, null, $replace); - if ($salesRule->getUseAutoGeneration() == 'Yes') { + if ($salesRule->getUseAutoGeneration() == 'Yes' && !empty($generateCouponsSettings)) { $this->promoQuoteNew->getFormPageActions()->saveAndContinue(); /** @var PromoQuoteForm $salesRuleForm */ $salesRuleForm = $this->promoQuoteNew->getSalesRuleForm(); - $salesRuleForm->fill($manageCouponCodes); + $salesRuleForm->generateCoupons($generateCouponsSettings); /** @var BlockPromoSalesRuleEditTabCoupons $manageCouponCodesSection */ $manageCouponCodesSection = $salesRuleForm->getSection('block_promo_sales_rule_edit_tab_coupons'); - $manageCouponCodesSection->pressGenerateButton(); /** @var Grid $couponGrid */ $couponGrid = $manageCouponCodesSection->getCouponGrid(); + /** @var array $generatedCouponCodes */ $generatedCouponCodes = $couponGrid->getRowsData(['code']); + $generatedCouponCodes = array_map( + function ($element) { + return $element['code']; + }, + $generatedCouponCodes + ); } else { $this->promoQuoteNew->getFormPageActions()->save(); } - return ['generatedCouponCodes' => $generatedCouponCodes]; + return ['salesRule' => $salesRule, 'couponCodes' => $generatedCouponCodes]; } /** diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index ece5e09c55493..430a31731dab5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -417,15 +417,14 @@ 50 No No - 1 + 1 simple_for_salesrule_1 - 5 US_address_1 checkmo Flat Rate Fixed - + diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml new file mode 100644 index 0000000000000..eafb758b22576 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt index 7bb9bd2cfaff1..cafac52cd5afe 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/reference.txt @@ -59,9 +59,7 @@ Model3 \Magento\TestModule2\Service\V1\Entity\ItemBuilder \Magento\TestModule4\Service\V1\Entity\DataObjectResponse \Magento\TestModule4\Service\V1\Entity\DataObjectRequest -\Magento\Mtf\ObjectManager \Magento\Mtf\ObjectManager\Config\Mapper\Dom -\Magento\Mtf\Block\BlockInterface \Magento\Mtf\Data\Argument\Interpreter\Constant \Magento\Mtf\Data\Argument\Interpreter\Boolean \Magento\Mtf\Data\Argument\Interpreter\StringType From 5178239f2c89b3ce2ce75adfc9e639f03319f339 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Tue, 11 Apr 2017 11:40:27 +0300 Subject: [PATCH 010/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../Checkout/Test/Block/Onepage/Payment/DiscountCodes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php index 0b7148538d5c5..ac3f31feade74 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Payment/DiscountCodes.php @@ -67,8 +67,8 @@ public function applyCouponCode($code) * * @return string */ - public function getCouponApplyingMessage() { - + public function getCouponApplyingMessage() + { return $this->_rootElement->find($this->couponApplyingMessage, Locator::SELECTOR_XPATH)->getText(); } } From 4818f9b8f239118f7b78ec33aaec9a83d7c1ea96 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Tue, 11 Apr 2017 14:12:11 +0300 Subject: [PATCH 011/363] MAGETWO-65161: [Backport] - "Uses per Coupon" limit does not work for auto generated coupons - for 2.1 --- .../BlockPromoSalesRuleEditTabCoupons/Grid.php | 17 +++++++++++++++++ .../SalesRule/Test/Fixture/SalesRule.xml | 8 +------- .../Test/TestCase/CreateSalesRuleEntityTest.php | 8 +------- .../TestSuite/InjectableTests/MAGETWO-65161.xml | 15 --------------- 4 files changed, 19 insertions(+), 29 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php index 36ffcbb3874a9..afcd3e8ad16db 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/Section/BlockPromoSalesRuleEditTabCoupons/Grid.php @@ -11,5 +11,22 @@ */ class Grid extends \Magento\Backend\Test\Block\Widget\Grid { + /** + * Return generated coupon codes as array of codes. + * + * @return array + */ + public function getCouponCodes() + { + /** @var array $generatedCouponCodes */ + $generatedCouponCodes = $this->getRowsData(['code']); + $generatedCouponCodes = array_map( + function ($element) { + return $element['code']; + }, + $generatedCouponCodes + ); + return $generatedCouponCodes; + } } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml index dd652d1f646fb..967ce8c7aff26 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Fixture/SalesRule.xml @@ -43,11 +43,5 @@ - - - - - - - + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index 005888a358a00..930ac94ffa37f 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -151,13 +151,7 @@ public function testCreateSalesRule( $couponGrid = $manageCouponCodesSection->getCouponGrid(); /** @var array $generatedCouponCodes */ - $generatedCouponCodes = $couponGrid->getRowsData(['code']); - $generatedCouponCodes = array_map( - function ($element) { - return $element['code']; - }, - $generatedCouponCodes - ); + $generatedCouponCodes = $couponGrid->getCouponCodes(); } else { $this->promoQuoteNew->getFormPageActions()->save(); } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml deleted file mode 100644 index eafb758b22576..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-65161.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 9f36abf2d83566e2c87081df32003d4f2aaee25c Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Thu, 13 Apr 2017 17:57:17 +0300 Subject: [PATCH 012/363] MAGETWO-64922: Missing appropriate payment informations --- .../Order/Payment/Collection.php | 31 +------ .../ConvertAdditionalInfoObserver.php | 80 +++++++++++++++++++ app/code/Magento/Sales/etc/events.xml | 3 + .../Block/Adminhtml/Order/View/Tab/Info.php | 21 +++++ .../Order/View/Tab/Info/PaymentInfoBlock.php | 40 ++++++++++ 5 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 app/code/Magento/Sales/Observer/ConvertAdditionalInfoObserver.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info/PaymentInfoBlock.php diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php index d0ad42bee9c99..f4a7414530c58 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php @@ -5,7 +5,6 @@ */ namespace Magento\Sales\Model\ResourceModel\Order\Payment; -use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Api\Data\OrderPaymentSearchResultInterface; use Magento\Sales\Model\ResourceModel\Order\Collection\AbstractCollection; @@ -68,7 +67,7 @@ protected function _construct() } /** - * Unserialize additional_information in each item + * Unserialize additional_information in each item. * * @return $this */ @@ -76,34 +75,8 @@ protected function _afterLoad() { foreach ($this->_items as $item) { $this->getResource()->unserializeFields($item); - if (!empty($item->getData(OrderPaymentInterface::ADDITIONAL_INFORMATION))) { - $additionalInfo = $this->convertAdditionalInfo( - $item->getData(OrderPaymentInterface::ADDITIONAL_INFORMATION) - ); - $item->setData(OrderPaymentInterface::ADDITIONAL_INFORMATION, $additionalInfo); - } } - return parent::_afterLoad(); - } - /** - * Convert multidimensional additional information array to single - * - * @param array $info - * @return array - */ - private function convertAdditionalInfo($info) - { - $result = []; - foreach ($info as $key => $item) { - if (is_array($item)) { - $result += $this->convertAdditionalInfo($item); - unset($info[$key]); - } else { - $result[$key] = $item; - } - } - - return $result; + return parent::_afterLoad(); } } diff --git a/app/code/Magento/Sales/Observer/ConvertAdditionalInfoObserver.php b/app/code/Magento/Sales/Observer/ConvertAdditionalInfoObserver.php new file mode 100644 index 0000000000000..f2853c30126ec --- /dev/null +++ b/app/code/Magento/Sales/Observer/ConvertAdditionalInfoObserver.php @@ -0,0 +1,80 @@ +state = $state; + } + + /** + * Convert additional info from multidimensional array into single one for API calls. + * + * @param Observer $observer + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + /** @var Collection $paymentCollection */ + $paymentCollection = $observer->getData('order_payment_collection'); + $areaCode = $this->state->getAreaCode(); + if ($areaCode == Area::AREA_WEBAPI_REST || $areaCode == Area::AREA_WEBAPI_SOAP) { + foreach ($paymentCollection as $payment) { + if (!empty($payment->getData(OrderPaymentInterface::ADDITIONAL_INFORMATION))) { + $additionalInfo = $this->convertAdditionalInfo( + $payment->getData(OrderPaymentInterface::ADDITIONAL_INFORMATION) + ); + $payment->setData(OrderPaymentInterface::ADDITIONAL_INFORMATION, $additionalInfo); + } + } + } + } + + /** + * Convert multidimensional additional information array to single. + * + * @param array $info + * @return array + */ + private function convertAdditionalInfo($info) + { + $result = []; + foreach ($info as $key => $item) { + if (is_array($item)) { + $result += $this->convertAdditionalInfo($item); + unset($info[$key]); + } else { + $result[$key] = $item; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Sales/etc/events.xml b/app/code/Magento/Sales/etc/events.xml index 464f5a615b84f..164d7a23267f0 100644 --- a/app/code/Magento/Sales/etc/events.xml +++ b/app/code/Magento/Sales/etc/events.xml @@ -51,4 +51,7 @@ + + + diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info.php index d6fb589a0663d..cf7204dd72fdc 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info.php @@ -7,6 +7,7 @@ namespace Magento\Sales\Test\Block\Adminhtml\Order\View\Tab; use Magento\Mtf\Block\Block; +use Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info\PaymentInfoBlock; /** * Order information tab block. @@ -20,6 +21,13 @@ class Info extends Block */ protected $orderStatus = '#order_status'; + /** + * Selector for 'Payment Information' block. + * + * @var string + */ + private $paymentInfoBlockSelector = '.order-payment-method'; + /** * Get order status from info block * @@ -29,4 +37,17 @@ public function getOrderStatus() { return $this->_rootElement->find($this->orderStatus)->getText(); } + + /** + * Returns Payment Information block. + * + * @return PaymentInfoBlock + */ + public function getPaymentInfoBlock() + { + return $this->blockFactory->create( + PaymentInfoBlock::class, + ['element' => $this->_rootElement->find($this->paymentInfoBlockSelector)] + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info/PaymentInfoBlock.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info/PaymentInfoBlock.php new file mode 100644 index 0000000000000..0d38cf5e19c05 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/View/Tab/Info/PaymentInfoBlock.php @@ -0,0 +1,40 @@ +_rootElement->getElements($this->info); + foreach ($elements as $row) { + $key = rtrim($row->find('th')->getText(), ':'); + $value = $row->find('td')->getText(); + $result[$key] = $value; + } + + return $result; + } +} From 1d0d5933661a8c5717807dfb6fc91f8f3a0c66a9 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Fri, 14 Apr 2017 10:38:22 +0300 Subject: [PATCH 013/363] MAGETWO-64922: Missing appropriate payment informations --- .../TestSuite/InjectableTests/MAGETWO-64922.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml new file mode 100644 index 0000000000000..594e2e902ba63 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 0f45ca14fcd9e771a359a5069dde8f87bb0c781a Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Fri, 14 Apr 2017 10:55:26 +0300 Subject: [PATCH 014/363] MAGETWO-64922: Missing appropriate payment informations --- .../TestSuite/InjectableTests/MAGETWO-64922.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml deleted file mode 100644 index 594e2e902ba63..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64922.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 9fc50a8a9a1f7555fca1ae049ab75bbad7623989 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Wed, 26 Apr 2017 11:41:17 +0300 Subject: [PATCH 015/363] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Category/Product/AbstractAction.php | 45 +++- .../Framework/DB/Adapter/Pdo/Mysql.php | 1 - .../Framework/DB/Query/BatchIterator.php | 2 +- .../DB/Query/BatchIteratorFactory.php | 4 +- .../DB/Query/BatchIteratorInterface.php | 68 ++++++ .../Framework/DB/Query/BatchRangeIterator.php | 208 ++++++++++++++++++ .../Magento/Framework/DB/Query/Generator.php | 118 +++++++++- .../Unit/DB/Query/BatchRangeIteratorTest.php | 123 +++++++++++ .../Test/Unit/DB/Query/GeneratorTest.php | 48 +++- 9 files changed, 593 insertions(+), 24 deletions(-) create mode 100644 lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php create mode 100644 lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php create mode 100644 lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index feb9f3bf8c343..250ea1d477a34 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Model\Indexer\Category\Product; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; @@ -102,20 +103,29 @@ abstract class AbstractAction */ protected $tempTreeIndexTableName; + /** + * @var QueryGenerator + */ + private $queryGenerator; + /** * @param ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Config $config + * @param QueryGenerator $queryGenerator */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config + \Magento\Catalog\Model\Config $config, + QueryGenerator $queryGenerator = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); $this->storeManager = $storeManager; $this->config = $config; + $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(QueryGenerator::class); } /** @@ -302,22 +312,35 @@ protected function isRangingNeeded() } /** - * Return selects cut by min and max + * Return selects cut by min and max. * * @param \Magento\Framework\DB\Select $select * @param string $field * @param int $range * @return \Magento\Framework\DB\Select[] */ - protected function prepareSelectsByRange(\Magento\Framework\DB\Select $select, $field, $range = self::RANGE_CATEGORY_STEP) - { - return $this->isRangingNeeded() ? $this->connection->selectsByRange( - $field, - $select, - $range - ) : [ - $select - ]; + protected function prepareSelectsByRange( + \Magento\Framework\DB\Select $select, + $field, + $range = self::RANGE_CATEGORY_STEP + ) { + if ($this->isRangingNeeded()) { + $iterator = $this->queryGenerator->generate( + $field, + $select, + $range, + \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + ); + + $queries = []; + foreach ($iterator as $query) { + $queries[] = $query; + } + + return $queries; + } + + return [$select]; } /** diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 3313bd25f06f8..a43f39a5be1a8 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -1350,7 +1350,6 @@ protected function _removeDuplicateEntry($table, $fields, $ids) */ public function select() { -// return new Select($this); return $this->selectFactory->create($this); } diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 4235eeb7a65e9..76e46d72d5c9c 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -11,7 +11,7 @@ /** * Query batch iterator */ -class BatchIterator implements \Iterator +class BatchIterator implements BatchIteratorInterface { /** * @var int diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php index 147e65b8df466..8a597512554e9 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php @@ -39,10 +39,10 @@ public function __construct( } /** - * Create class instance with specified parameters + * Create class instance with specified parameters. * * @param array $data - * @return \Magento\Framework\DB\Query\BatchIterator + * @return \Magento\Framework\DB\Query\BatchIteratorInterface */ public function create(array $data = []) { diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php new file mode 100644 index 0000000000000..9ab94a4c5ac0a --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php @@ -0,0 +1,68 @@ +batchSize = $batchSize; + $this->select = $select; + $this->correlationName = $correlationName; + $this->rangeField = $rangeField; + $this->rangeFieldAlias = $rangeFieldAlias; + $this->connection = $select->getConnection(); + } + + /** + * Return the current element. + * + * If we don't have sub-select we should create and remember it. + * + * @return Select + */ + public function current() + { + if (null === $this->currentSelect) { + $this->isValid = ($this->currentBatch + $this->batchSize) < $this->totalItemCount; + $this->currentSelect = $this->initSelectObject(); + } + return $this->currentSelect; + } + + /** + * Return the key of the current element. + * + * Сan return the number of the current sub-select in the iteration. + * + * @return int + */ + public function key() + { + return $this->iteration; + } + + /** + * Move forward to next sub-select + * + * Retrieve the next sub-select and move cursor to the next element. + * Checks that the count of elements more than the sum of limit and offset. + * + * @return Select + */ + public function next() + { + if (null === $this->currentSelect) { + $this->current(); + } + $this->isValid = ($this->batchSize + $this->currentBatch) < $this->totalItemCount; + $select = $this->initSelectObject(); + if ($this->isValid) { + $this->iteration++; + $this->currentSelect = $select; + } else { + $this->currentSelect = null; + } + return $this->currentSelect; + } + + /** + * Rewind the BatchRangeIterator to the first element. + * + * Allows to start iteration from the beginning. + * + * @return void + */ + public function rewind() + { + $this->currentSelect = null; + $this->iteration = 0; + $this->isValid = true; + $this->totalItemCount = 0; + } + + /** + * Checks if current position is valid. + * + * @return bool + */ + public function valid() + { + return $this->isValid; + } + + /** + * Initialize select object. + * + * Return sub-select which is limited by current batch value and return items from n page of SQL request. + * + * @return \Magento\Framework\DB\Select + */ + private function initSelectObject() + { + $object = clone $this->select; + + if (!$this->totalItemCount) { + $wrapperSelect = $this->connection->select(); + $wrapperSelect->from( + $object, + [ + new \Zend_Db_Expr('COUNT(*) as cnt') + ] + ); + $row = $this->connection->fetchRow($wrapperSelect); + + $this->totalItemCount = intval($row['cnt']); + } + + //Reset sort order section from origin select object. + $object->order($this->correlationName . '.' . $this->rangeField . ' ' . \Magento\Framework\DB\Select::SQL_ASC); + $object->limit($this->currentBatch, $this->batchSize); + $this->currentBatch += $this->batchSize; + + return $object; + } +} diff --git a/lib/internal/Magento/Framework/DB/Query/Generator.php b/lib/internal/Magento/Framework/DB/Query/Generator.php index 909f4752397a3..49ec98c317976 100644 --- a/lib/internal/Magento/Framework/DB/Query/Generator.php +++ b/lib/internal/Magento/Framework/DB/Query/Generator.php @@ -17,27 +17,61 @@ class Generator */ private $iteratorFactory; + /** + * @var \Magento\Framework\DB\Query\BatchRangeIteratorFactory + */ + private $rangeIteratorFactory; + /** * Initialize dependencies. * * @param BatchIteratorFactory $iteratorFactory + * @param BatchRangeIteratorFactory $rangeIteratorFactory */ - public function __construct(BatchIteratorFactory $iteratorFactory) - { + public function __construct( + BatchIteratorFactory $iteratorFactory, + BatchRangeIteratorFactory $rangeIteratorFactory = null + ) { $this->iteratorFactory = $iteratorFactory; + $this->rangeIteratorFactory = $rangeIteratorFactory ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\DB\Query\BatchRangeIteratorFactory::class); } /** * Generate select query list with predefined items count in each select item. * - * @param string $rangeField + * Generates select parameters - batchSize, correlationName, rangeField, rangeFieldAlias + * to obtain instance of iterator. The behavior of the iterator will depend on the parameters passed to it. + * For example: by default for $batchStrategy parameter used + * \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR. This parameter is determine, what + * instance of Iterator will be returned. + * + * Other params: + * select - represents the select object, that should be passed into Iterator. + * batchSize - sets the number of items in select. + * correlationName - is the base table involved in the select. + * rangeField - this is the basic field which used to split select. + * rangeFieldAlias - alias of range field. + * + * @see \Magento\Framework\DB\Query\BatchIteratorInterface + * @param string $rangeField - Field which is used for the range mechanism in select * @param \Magento\Framework\DB\Select $select - * @param int $batchSize - * @return BatchIterator - * @throws LocalizedException + * @param int $batchSize - Determines on how many parts will be divided + * the number of values in the select. + * @param string $batchStrategy It determines which strategy is chosen + * @return BatchIteratorInterface + * @throws LocalizedException Throws if incorrect "FROM" part in \Select exists */ - public function generate($rangeField, \Magento\Framework\DB\Select $select, $batchSize = 100) - { + public function generate( + $rangeField, + \Magento\Framework\DB\Select $select, + $batchSize = 100, + $batchStrategy = \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR + ) { + if ($batchStrategy == \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR) { + return $this->generateByRange($rangeField, $select, $batchSize); + } + $fromSelect = $select->getPart(\Magento\Framework\DB\Select::FROM); if (empty($fromSelect)) { throw new LocalizedException( @@ -76,4 +110,72 @@ public function generate($rangeField, \Magento\Framework\DB\Select $select, $bat ] ); } + + /** + * Generate select query list with predefined items count in each select item. + * + * Generates select parameters - batchSize, correlationName, rangeField, rangeFieldAlias + * to obtain instance of BatchRangeIterator. + * + * Other params: + * select - represents the select object, that should be passed into Iterator. + * batchSize - sets the number of items in select. + * correlationName - is the base table involved in the select. + * rangeField - this is the basic field which used to split select. + * rangeFieldAlias - alias of range field. + * + * @see BatchRangeIterator + * @param string $rangeField - Field which is used for the range mechanism in select. + * @param \Magento\Framework\DB\Select $select + * @param int $batchSize + * @return BatchIteratorInterface + * @throws LocalizedException Throws if incorrect "FROM" part in \Select exists + * @see \Magento\Framework\DB\Query\Generator + * @deprecated This is a temporary solution which is made due to the fact that we + * can't change method generate() in version 2.1 due to a backwards incompatibility. + * In 2.2 version need to use original method generate() with additional parameter. + */ + public function generateByRange( + $rangeField, + \Magento\Framework\DB\Select $select, + $batchSize = 100 + ) { + $fromSelect = $select->getPart(\Magento\Framework\DB\Select::FROM); + if (empty($fromSelect)) { + throw new LocalizedException( + new \Magento\Framework\Phrase('Select object must have correct "FROM" part') + ); + } + + $fieldCorrelationName = ''; + foreach ($fromSelect as $correlationName => $fromPart) { + if ($fromPart['joinType'] == \Magento\Framework\DB\Select::FROM) { + $fieldCorrelationName = $correlationName; + break; + } + } + + $columns = $select->getPart(\Magento\Framework\DB\Select::COLUMNS); + /** + * Calculate $rangeField alias + */ + $rangeFieldAlias = $rangeField; + foreach ($columns as $column) { + list($table, $columnName, $alias) = $column; + if ($table == $fieldCorrelationName && $columnName == $rangeField) { + $rangeFieldAlias = $alias ?: $rangeField; + break; + } + } + + return $this->rangeIteratorFactory->create( + [ + 'select' => $select, + 'batchSize' => $batchSize, + 'correlationName' => $fieldCorrelationName, + 'rangeField' => $rangeField, + 'rangeFieldAlias' => $rangeFieldAlias, + ] + ); + } } diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php new file mode 100644 index 0000000000000..18e91e0264738 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php @@ -0,0 +1,123 @@ +batchSize = 10; + $this->currentBatch = 0; + $this->correlationName = 'correlationName'; + $this->rangeField = 'rangeField'; + $this->rangeFieldAlias = 'rangeFieldAlias'; + + $this->selectMock = $this->getMock(Select::class, [], [], '', false, false); + $this->wrapperSelectMock = $this->getMock(Select::class, [], [], '', false, false); + $this->connectionMock = $this->getMock(AdapterInterface::class); + $this->connectionMock->expects($this->any())->method('select')->willReturn($this->wrapperSelectMock); + $this->selectMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + $this->connectionMock->expects($this->any())->method('quoteIdentifier')->willReturnArgument(0); + + $this->model = new BatchRangeIterator( + $this->selectMock, + $this->batchSize, + $this->correlationName, + $this->rangeField, + $this->rangeFieldAlias + ); + } + + /** + * Test steps: + * 1. $iterator->current(); + * 2. $iterator->key(); + * + * @return void + */ + public function testCurrent() + { + $this->selectMock->expects($this->once())->method('limit')->with($this->currentBatch, $this->batchSize); + $this->selectMock->expects($this->once())->method('order')->with('correlationName.rangeField' . ' ASC'); + + $this->assertEquals($this->selectMock, $this->model->current()); + $this->assertEquals(0, $this->model->key()); + } + + /** + * Test the separation of batches. + */ + public function testIterations() + { + $iterations = 0; + + $this->connectionMock->expects($this->once()) + ->method('fetchRow') + ->willReturn(['cnt' => 105]); + + foreach ($this->model as $key) { + $this->assertEquals($this->selectMock, $key); + $iterations++; + }; + + $this->assertEquals(10, $iterations); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php index fcfaa41ffb4e6..ac658930c47ec 100644 --- a/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php @@ -7,6 +7,7 @@ use Magento\Framework\DB\Query\Generator; use Magento\Framework\DB\Query\BatchIteratorFactory; +use Magento\Framework\DB\Query\BatchRangeIteratorFactory; use Magento\Framework\DB\Select; use Magento\Framework\DB\Query\BatchIterator; @@ -32,15 +33,21 @@ class GeneratorTest extends \PHPUnit_Framework_TestCase */ private $iteratorMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $rangeFactoryMock; + /** * Setup test dependencies. */ protected function setUp() { $this->factoryMock = $this->getMock(BatchIteratorFactory::class, [], [], '', false, false); + $this->rangeFactoryMock = $this->getMock(BatchRangeIteratorFactory::class, ['create'], [], '', false, false); $this->selectMock = $this->getMock(Select::class, [], [], '', false, false); $this->iteratorMock = $this->getMock(BatchIterator::class, [], [], '', false, false); - $this->model = new Generator($this->factoryMock); + $this->model = new Generator($this->factoryMock, $this->rangeFactoryMock); } /** @@ -165,4 +172,43 @@ public function testGenerateWithInvalidWithWildcard() )->willReturn($this->iteratorMock); $this->assertEquals($this->iteratorMock, $this->model->generate('entity_id', $this->selectMock, 100)); } + + /** + * Test success generate with non-unique strategy. + * @return void + */ + public function testGenerateWithNonUniqueStrategy() + { + $map = [ + [ + Select::FROM, + [ + 'cp' => ['joinType' => Select::FROM] + ] + ], + [ + Select::COLUMNS, + [ + ['cp', 'entity_id', 'product_id'] + ] + ] + ]; + + $this->selectMock->expects($this->exactly(2))->method('getPart')->willReturnMap($map); + + $this->factoryMock->expects($this->once())->method('create')->with( + [ + 'select' => $this->selectMock, + 'batchSize' => 100, + 'correlationName' => 'cp', + 'rangeField' => 'entity_id', + 'rangeFieldAlias' => 'product_id' + ] + )->willReturn($this->iteratorMock); + + $this->assertEquals( + $this->iteratorMock, + $this->model->generate('entity_id', $this->selectMock, 100, 'non_unique') + ); + } } From 66240af7e6895c38f031d53f0f3c7b695238ea30 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Wed, 26 Apr 2017 14:42:44 +0300 Subject: [PATCH 016/363] MAGETWO-65249: [Backport] - [Performance] Segmentation fault when doing catalogsearch_fulltext reindex - for 2.1 --- .../Model/Indexer/Fulltext/Action/Full.php | 187 ++++++++++++++++-- .../Indexer/Fulltext/Action/IndexIterator.php | 12 ++ .../Indexer/Fulltext/Action/FullTest.php | 85 +++++++- .../product_configurable_not_available.php | 105 ++++++++++ ...ct_configurable_not_available_rollback.php | 34 ++++ .../_files/products_for_index.php | 77 ++++++++ .../_files/products_for_index_rollback.php | 40 ++++ .../_files/configurable_products.php | 161 +++++++++++++++ .../_files/configurable_products_rollback.php | 35 ++++ .../Search/_files/configurable_attribute.php | 60 ++++++ .../configurable_attribute_rollback.php | 30 +++ .../Search/_files/product_configurable.php | 120 +++++++++++ .../_files/product_configurable_rollback.php | 31 +++ 13 files changed, 949 insertions(+), 28 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 43a9549c46293..e08954806da29 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -6,9 +6,12 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; /** + * Class provides iterator through number of products suitable for fulltext indexation + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -30,6 +33,8 @@ class Full * Index values separator * * @var string + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -37,6 +42,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] + * @deprecated Not used anymore */ protected $dates = []; @@ -44,6 +50,8 @@ class Full * Product Type Instances cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -51,6 +59,8 @@ class Full * Product Emulators cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -77,6 +87,8 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -91,6 +103,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @deprecated Not used anymore */ protected $scopeConfig; @@ -98,6 +111,8 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -108,21 +123,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface + * @deprecated As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime + * @deprecated Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface + * @deprecated Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @deprecated Not used anymore */ protected $localeDate; @@ -133,16 +152,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext + * @deprecated Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config + * @deprecated As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory + * @deprecated As part of self::cleanIndex() */ private $dimensionFactory; @@ -153,6 +175,8 @@ class Full /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory + * @deprecated DataProvider used directly without IndexIterator + * @see self::$dataProvider */ private $iteratorFactory; @@ -161,6 +185,11 @@ class Full */ private $metadataPool; + /** + * @var DataProvider + */ + private $dataProvider; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -181,6 +210,7 @@ class Full * @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig * @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param DataProvider $dataProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +232,8 @@ public function __construct( \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory, \Magento\Framework\Indexer\ConfigInterface $indexerConfig, \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null + \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + DataProvider $dataProvider = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -223,13 +254,16 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->dimensionFactory = $dimensionFactory; $this->iteratorFactory = $indexIteratorFactory; - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class); } /** * Rebuild whole fulltext index for all stores * + * @deprecated Please use \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull instead + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull * @return void */ public function reindexAll() @@ -282,12 +316,14 @@ protected function getProductIdsFromParents(array $entityIds) /** * Regenerate search index for specific store * + * To be suitable for indexation product must meet set of requirements: + * - to be visible on frontend + * - to be enabled + * - in case product is composite at least one sub product must be enabled + * * @param int $storeId Store View Id - * @param int|array $productIds Product Entity Id + * @param int[] $productIds Product Entity Id * @return \Generator - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function rebuildStoreIndex($storeId, $productIds = null) { @@ -307,27 +343,129 @@ public function rebuildStoreIndex($storeId, $productIds = null) 'datetime' => array_keys($this->getSearchableAttributes('datetime')), ]; - // status and visibility filter + $lastProductId = 0; + do { + $products = $this->dataProvider + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + + $productsIds = array_column($products, 'entity_id'); + $relatedProducts = $this->getRelatedProducts($products); + $productsIds = array_merge($productsIds, array_values($relatedProducts)); + + $productsAttributes = $this->dataProvider->getProductAttributes($storeId, $productsIds, $dynamicFields); + + foreach ($products as $productData) { + $lastProductId = $productData['entity_id']; + + if (!$this->isProductVisible($productData['entity_id'], $productsAttributes) || + !$this->isProductEnabled($productData['entity_id'], $productsAttributes) + ) { + continue; + } + + $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]]; + if (isset($relatedProducts[$productData['entity_id']])) { + $childProductsIndex = $this->getChildProductsIndex( + $productData['entity_id'], + $relatedProducts, + $productsAttributes + ); + if (empty($childProductsIndex)) { + continue; + } + $productIndex = $productIndex + $childProductsIndex; + } + + $index = $this->dataProvider->prepareProductIndex($productIndex, $productData, $storeId); + yield $productData['entity_id'] => $index; + } + } while (count($products) > 0); + } + + /** + * Get related (child) products ids + * + * Load related (child) products ids for composite product type + * + * @param array $products + * @return array + */ + private function getRelatedProducts($products) + { + $relatedProducts = []; + foreach ($products as $productData) { + $relatedProducts[$productData['entity_id']] = $this->dataProvider->getProductChildIds( + $productData['entity_id'], + $productData['type_id'] + ); + } + return array_filter($relatedProducts); + } + + /** + * Performs check that product is visible on Store Front + * + * Check that product is visible on Store Front using visibility attribute + * and allowed visibility values. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductVisible($productId, array $productsAttributes) + { $visibility = $this->getSearchableAttribute('visibility'); - $status = $this->getSearchableAttribute('status'); - $statusIds = $this->catalogProductStatus->getVisibleStatusIds(); $allowedVisibility = $this->engine->getAllowedVisibility(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$visibility->getId()]) && + in_array($productsAttributes[$productId][$visibility->getId()], $allowedVisibility); + } - return $this->iteratorFactory->create([ - 'storeId' => $storeId, - 'productIds' => $productIds, - 'staticFields' => $staticFields, - 'dynamicFields' => $dynamicFields, - 'visibility' => $visibility, - 'allowedVisibility' => $allowedVisibility, - 'status' => $status, - 'statusIds' => $statusIds - ]); + /** + * Performs check that product is enabled on Store Front + * + * Check that product is enabled on Store Front using status attribute + * and statuses allowed to be visible on Store Front. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductEnabled($productId, array $productsAttributes) + { + $status = $this->getSearchableAttribute('status'); + $allowedStatuses = $this->catalogProductStatus->getVisibleStatusIds(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$status->getId()]) && + in_array($productsAttributes[$productId][$status->getId()], $allowedStatuses); + } + + /** + * Get data for index using related(child) products data + * + * Build data for index using child products(if any). + * Use only enabled child products {@see isProductEnabled}. + * + * @param int $parentId + * @param array $relatedProducts + * @param array $productsAttributes + * @return array + */ + private function getChildProductsIndex($parentId, array $relatedProducts, array $productsAttributes) + { + $productIndex = []; + foreach ($relatedProducts[$parentId] as $productChildId) { + if ($this->isProductEnabled($productChildId, $productsAttributes)) { + $productIndex[$productChildId] = $productsAttributes[$productChildId]; + } + } + return $productIndex; } /** * Clean search index data for store * + * @deprecated As part of self::reindexAll() * @param int $storeId * @return void */ @@ -341,6 +479,7 @@ protected function cleanIndex($storeId) * Retrieve EAV Config Singleton * * @return \Magento\Eav\Model\Config + * @deprecated Use $self::$eavConfig directly */ protected function getEavConfig() { @@ -369,7 +508,7 @@ protected function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->getEavConfig()->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -413,12 +552,14 @@ protected function getSearchableAttribute($attribute) } } - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); } /** * Returns expression for field unification * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::unifyField() * @param string $field * @param string $backendType * @return \Zend_Db_Expr @@ -436,6 +577,8 @@ protected function unifyField($field, $backendType = 'varchar') /** * Retrieve Product Type Instance * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductTypeInstance() * @param string $typeId * @return \Magento\Catalog\Model\Product\Type\AbstractType */ @@ -452,6 +595,8 @@ protected function getProductTypeInstance($typeId) /** * Retrieve Product Emulator (Magento Object) * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductEmulator() * @param string $typeId * @return \Magento\Framework\DataObject */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index ef223a774cb97..7b98257bd9c1f 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -10,6 +10,8 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @deprecated No more used + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ class IndexIterator implements \Iterator { @@ -133,6 +135,8 @@ public function __construct( /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function current() { @@ -141,6 +145,8 @@ public function current() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function next() { @@ -237,6 +243,8 @@ public function next() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function key() { @@ -245,6 +253,8 @@ public function key() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function valid() { @@ -253,6 +263,8 @@ public function valid() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function rewind() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 8ee4f8dd7d61b..174d1f015fc58 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -5,26 +5,97 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogSearch\Model\ResourceModel\Engine; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; -/** - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - */ class FullTest extends \PHPUnit_Framework_TestCase { /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ - protected $fullAction; + protected $actionFull; protected function setUp() { - $this->fullAction = Bootstrap::getObjectManager()->create( + $this->actionFull = Bootstrap::getObjectManager()->create( \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class ); } + /** + * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php + * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php + */ + public function testGetIndexData() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); + $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); + + $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); + $this->assertNotEmpty($result); + + $productsIds = array_keys($result); + foreach ($productsIds as $productId) { + $product = $productRepository->getById($productId); + $this->assertContains($product->getVisibility(), $allowedVisibility); + $this->assertContains($product->getStatus(), $allowedStatuses); + } + + $expectedData = $this->getExpectedIndexData(); + foreach ($expectedData as $sku => $expectedIndexData) { + $product = $productRepository->get($sku); + $this->assertEquals($expectedIndexData, $result[$product->getId()]); + } + } + + /** + * @return array + */ + private function getExpectedIndexData() + { + /** @var ProductAttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->get(ProductAttributeRepositoryInterface::class); + $skuId = $attributeRepository->get(ProductInterface::SKU)->getAttributeId(); + $nameId = $attributeRepository->get(ProductInterface::NAME)->getAttributeId(); + /** @see dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php */ + $configurableId = $attributeRepository->get('test_configurable_searchable')->getAttributeId(); + return [ + 'configurable_searchable' => [ + $skuId => 'configurable_searchable', + $configurableId => 'Option 1 | Option 2', + $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', + ], + 'index_enabled' => [ + $skuId => 'index_enabled', + $nameId => 'index enabled', + ], + 'index_visible_search' => [ + $skuId => 'index_visible_search', + $nameId => 'index visible search', + ], + 'index_visible_category' => [ + $skuId => 'index_visible_category', + $nameId => 'index visible category', + ], + 'index_visible_both' => [ + $skuId => 'index_visible_both', + $nameId => 'index visible both', + ] + ]; + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ public function testRebuildStoreIndexConfigurable() { $storeId = 1; @@ -36,8 +107,8 @@ public function testRebuildStoreIndexConfigurable() $simpleProductId, $configProductId ]; - $storeIndexDataSimple = $this->fullAction->rebuildStoreIndex($storeId, [$simpleProductId]); - $storeIndexDataExpected = $this->fullAction->rebuildStoreIndex($storeId, $expected); + $storeIndexDataSimple = $this->actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); + $storeIndexDataExpected = $this->actionFull->rebuildStoreIndex($storeId, $expected); $this->assertEquals($storeIndexDataSimple, $storeIndexDataExpected); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php new file mode 100644 index 0000000000000..a5d5e42e077f8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php @@ -0,0 +1,105 @@ +reinitialize(); + +require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productsSku = [1110, 1120]; +array_shift($options); //remove the first option which is empty + +$isFirstOption = true; +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productSku = array_shift($productsSku); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_not_avalilable_' . $productSku) + ->setPrice(11) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_DISABLED); + $product = $productRepository->save($product); + + /** @var StockItemInterface $stockItem */ + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $stockItem->setUseConfigManageStock(1)->setIsInStock(true)->setQty(100)->setIsQtyDecimal(0); + + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $isFirstOption = false; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Product Disable Child Products') + ->setSku('configurable_not_available') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +$product = $productRepository->save($product); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setUseConfigManageStock(1)->setIsInStock(1); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php new file mode 100644 index 0000000000000..3cca01c289362 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php @@ -0,0 +1,34 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$productSkus = ['simple_not_avalilable_1110', 'simple_not_avalilable_1120', 'configurable_not_available']; +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, $productSkus, 'in'); +$result = $productRepository->getList($searchCriteriaBuilder->create()); +foreach ($result->getItems() as $product) { + $productRepository->delete($product); +} + +require __DIR__ . '/../../../Magento/Framework/Search/_files/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php new file mode 100644 index 0000000000000..6d6c658c1390a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php @@ -0,0 +1,77 @@ +get(ProductRepositoryInterface::class); +$products = [ + [ + 'name' => 'index enabled', + 'sku' => 'index_enabled', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + 'name' => 'index disabled', + 'sku' => 'index_disabled', + 'status' => Status::STATUS_DISABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + 'name' => 'index visible search', + 'sku' => 'index_visible_search', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_IN_SEARCH, + ], + [ + 'name' => 'index visible category', + 'sku' => 'index_visible_category', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_IN_CATALOG, + ], + [ + 'name' => 'index visible both', + 'sku' => 'index_visible_both', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + + 'name' => 'index not visible', + 'sku' => 'index_not_visible', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE, + + ] +]; + +/** @var $productFactory ProductInterfaceFactory */ +$productFactory = Bootstrap::getObjectManager()->create(ProductInterfaceFactory::class); +foreach ($products as $data) { + /** @var ProductInterface $product */ + $product = $productFactory->create(); + $product + ->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName($data['name']) + ->setSku($data['sku']) + ->setPrice(10) + ->setVisibility($data['visibility']) + ->setStatus($data['status']); + $product = $productRepository->save($product); + + /** @var StockItemInterface $stockItem */ + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $stockItem->setUseConfigManageStock(0); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php new file mode 100644 index 0000000000000..057d2e63aacc6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php @@ -0,0 +1,40 @@ +getInstance()->reinitialize(); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + +$productSkus = [ + 'index_enabled', + 'index_disabled', + 'index_visible_search', + 'index_visible_category', + 'index_visible_both', + 'index_not_visible' +]; +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, $productSkus, 'in'); +$result = $productRepository->getList($searchCriteriaBuilder->create()); +foreach ($result->getItems() as $product) { + $productRepository->delete($product); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php new file mode 100644 index 0000000000000..bcb5de33dea25 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -0,0 +1,161 @@ +get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php new file mode 100644 index 0000000000000..685e5f3a2e883 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable', 'simple_30', 'simple_40', 'configurable_12345'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php new file mode 100644 index 0000000000000..fab31aa9d79a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php @@ -0,0 +1,60 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_searchable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable_searchable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute); +} + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php new file mode 100644 index 0000000000000..70dc0352b938b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php @@ -0,0 +1,30 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_searchable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php new file mode 100644 index 0000000000000..0a1d018a8ea0d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -0,0 +1,120 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10010, 10020]; +array_shift($options); //remove the first option which is empty + +$isFirstOption = true; +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurableSearchable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => (int)!$isFirstOption, + ] + ); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock((int)!$isFirstOption); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $isFirstOption = false; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(10001) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable_searchable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php new file mode 100644 index 0000000000000..3262b6c9da35c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php @@ -0,0 +1,31 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10010', 'simple_10020', 'configurable_searchable'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 5b98a0d10d57154139f60e344e7401082cf95f9e Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Wed, 26 Apr 2017 15:15:00 +0300 Subject: [PATCH 017/363] MAGETWO-65249: [Backport] - [Performance] Segmentation fault when doing catalogsearch_fulltext reindex - for 2.1 --- .../Model/Indexer/Fulltext/Action/IndexIterator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index 7b98257bd9c1f..7f4b5f517ac9d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -132,7 +132,6 @@ public function __construct( $this->statusIds = $statusIds; } - /** * {@inheritDoc} * From 999bdd934ee53843f46444f189671accbd885488 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Wed, 26 Apr 2017 18:06:13 +0300 Subject: [PATCH 018/363] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Magento/Framework/DB/Query/BatchIterator.php | 10 +++++----- .../Framework/DB/Query/BatchIteratorInterface.php | 8 ++++---- .../Magento/Framework/DB/Query/BatchRangeIterator.php | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 76e46d72d5c9c..f6caf63b84d91 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -88,7 +88,7 @@ public function __construct( } /** - * @return Select + * {@inheritdoc} */ public function current() { @@ -101,7 +101,7 @@ public function current() } /** - * @return Select + * {@inheritdoc} */ public function next() { @@ -121,7 +121,7 @@ public function next() } /** - * @return int + * {@inheritdoc} */ public function key() { @@ -129,7 +129,7 @@ public function key() } /** - * @return bool + * {@inheritdoc} */ public function valid() { @@ -137,7 +137,7 @@ public function valid() } /** - * @return void + * {@inheritdoc} */ public function rewind() { diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php index 9ab94a4c5ac0a..a78cfebf3659d 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php @@ -11,13 +11,13 @@ interface BatchIteratorInterface extends \Iterator { /** - * Constant which determine strategy to create iterator which will to process + * Constant which determines strategy to create iterator which will process * range field eg. entity_id with unique values. */ const UNIQUE_FIELD_ITERATOR = "unique"; /** - * Constant which determine strategy to create iterator which will to process + * Constant which determines strategy to create iterator which will process * range field with non-unique values. */ const NON_UNIQUE_FIELD_ITERATOR = "non_unqiue"; @@ -25,7 +25,7 @@ interface BatchIteratorInterface extends \Iterator /** * Return the current element. * - * If we don't have sub-select we should create and remember it. + * If we don't have sub-select, we should create and remember it. * * @return \Magento\Framework\DB\Select */ @@ -34,7 +34,7 @@ public function current(); /** * Return the key of the current element. * - * Сan return the number of the current sub-select in the iteration. + * Сan return the number of the current sub-selects in the iteration. * * @return int */ diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index 3a9546e61f72e..bd61f2528301f 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -12,8 +12,8 @@ /** * Query batch range iterator. * - * It is uses to processing selects which will obtain values from $rangeField with relation one-to-many. - * This iterator make chunks with operator LIMIT...OFFSET, + * It is used for processing selects which will obtain values from $rangeField with relation one-to-many. + * This iterator makes chunks with operator LIMIT...OFFSET, * starting with zero offset and finishing on OFFSET + LIMIT = TOTAL_COUNT. * * @see \Magento\Framework\DB\Query\Generator @@ -116,7 +116,7 @@ public function current() /** * Return the key of the current element. * - * Сan return the number of the current sub-select in the iteration. + * Сan return the number of the current sub-selects in the iteration. * * @return int */ @@ -126,10 +126,10 @@ public function key() } /** - * Move forward to next sub-select + * Move forward to next sub-select. * * Retrieve the next sub-select and move cursor to the next element. - * Checks that the count of elements more than the sum of limit and offset. + * Check that the count of elements more than the sum of limit and offset. * * @return Select */ From a3e30d1deaac033abbc644c2693c903305e3d424 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Thu, 27 Apr 2017 10:24:44 +0300 Subject: [PATCH 019/363] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Framework/DB/Query/BatchRangeIterator.php | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index bd61f2528301f..8bab558e3d92c 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -98,11 +98,7 @@ public function __construct( } /** - * Return the current element. - * - * If we don't have sub-select we should create and remember it. - * - * @return Select + * {@inheritdoc} */ public function current() { @@ -114,11 +110,7 @@ public function current() } /** - * Return the key of the current element. - * - * Сan return the number of the current sub-selects in the iteration. - * - * @return int + * {@inheritdoc} */ public function key() { @@ -126,12 +118,7 @@ public function key() } /** - * Move forward to next sub-select. - * - * Retrieve the next sub-select and move cursor to the next element. - * Check that the count of elements more than the sum of limit and offset. - * - * @return Select + * {@inheritdoc} */ public function next() { @@ -150,11 +137,7 @@ public function next() } /** - * Rewind the BatchRangeIterator to the first element. - * - * Allows to start iteration from the beginning. - * - * @return void + * {@inheritdoc} */ public function rewind() { @@ -165,9 +148,7 @@ public function rewind() } /** - * Checks if current position is valid. - * - * @return bool + * {@inheritdoc} */ public function valid() { From 385330ff83f8081824162fcea5873342331b4bb5 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 28 Apr 2017 14:03:44 +0300 Subject: [PATCH 020/363] MAGETWO-53675: [Backport] [GitHub] Sorting configurable products by price doesn't work when simple product has special_price #4778 - for 2.1 --- .../Product/Indexer/Price/DefaultPrice.php | 28 +++- .../Product/Indexer/Price/Configurable.php | 118 ++++------------- .../_files/enable_price_index_schedule.php | 10 ++ .../enable_price_index_schedule_rollback.php | 10 ++ .../Pricing/Price/SpecialPriceIndexerTest.php | 101 +++++++++++++++ .../Pricing/Price/SpecialPriceTest.php | 121 ++++++++++++++++++ 6 files changed, 287 insertions(+), 101 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 41456945191fa..8a97d43a18d7e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -235,11 +235,30 @@ protected function _prepareFinalPriceData($entityIds = null) * @param string|null $type product type, all if null * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function prepareFinalPriceDataForType($entityIds, $type) { $this->_prepareDefaultFinalPriceTable(); + + $select = $this->getSelect($entityIds, $type); + $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); + $this->getConnection()->query($query); + return $this; + } + + /** + * Forms Select for collecting price related data for final price index table + * Next types of prices took into account: default, special, tier price + * Moved to protected for possible reusing + * + * @param int|array $entityIds Ids for filtering output result + * @param string|null $type Type for filtering output result by specified product type (all if null) + * @return \Magento\Framework\DB\Select + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function getSelect($entityIds = null, $type = null) + { $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $connection = $this->getConnection(); $select = $connection->select()->from( @@ -368,13 +387,10 @@ protected function prepareFinalPriceDataForType($entityIds, $type) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('e.entity_id'), 'website_field' => new \Zend_Db_Expr('cw.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); - - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); - $connection->query($query); - return $this; + return $select; } /** diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index f6fbccbab4cea..7f4f0f9e9376f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -8,11 +8,11 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; -use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Store\Api\StoreResolverInterface; -use Magento\Store\Model\Store; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice { /** @@ -29,6 +29,7 @@ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\ * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Module\Manager $moduleManager * @param string $connectionName + * @param StoreResolverInterface $storeResolver */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -83,63 +84,14 @@ public function reindexEntity($entityIds) protected function reindex($entityIds = null) { if ($this->hasEntity() || !empty($entityIds)) { - if (!empty($entityIds)) { - $allEntityIds = $this->getRelatedProducts($entityIds); - $this->prepareFinalPriceDataForType($allEntityIds, null); - } else { - $this->_prepareFinalPriceData($entityIds); - } + $this->prepareFinalPriceDataForType($entityIds, $this->getTypeId()); $this->_applyCustomOption(); - $this->_applyConfigurableOption($entityIds); + $this->_applyConfigurableOption(); $this->_movePriceDataToIndexTable($entityIds); } return $this; } - /** - * Get related product - * - * @param int[] $entityIds - * @return int[] - */ - private function getRelatedProducts($entityIds) - { - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - $select = $this->getConnection()->select()->union( - [ - $this->getConnection()->select() - ->from( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id' - )->join( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.product_id' - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from($this->getTable('catalog_product_super_link'), 'product_id') - ->where('product_id in (?)', $entityIds), - ] - ); - - return array_map('intval', $this->getConnection()->fetchCol($select)); - } - /** * Retrieve table name for custom option temporary aggregation data * @@ -195,56 +147,32 @@ protected function _applyConfigurableOption() $connection = $this->getConnection(); $coaTable = $this->_getConfigurableOptionAggregateTable(); $copTable = $this->_getConfigurableOptionPriceTable(); + $linkField = $metadata->getLinkField(); $this->_prepareConfigurableOptionAggregateTable(); $this->_prepareConfigurableOptionPriceTable(); - $statusAttribute = $this->_getAttribute(ProductInterface::STATUS); - $linkField = $metadata->getLinkField(); - - $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], - [] - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id = i.entity_id', - ['parent_id' => 'e.entity_id'] - )->join( + $subSelect = $this->getSelect(); + $subSelect->join( ['l' => $this->getTable('catalog_product_super_link')], - 'l.parent_id = e.' . $linkField, - ['product_id'] - )->columns( - ['customer_group_id', 'website_id'], - 'i' + 'l.product_id = e.entity_id', + [] )->join( ['le' => $this->getTable('catalog_product_entity')], - 'le.entity_id = l.product_id', - [] - )->where( - 'le.required_options=0' - )->joinLeft( - ['status_global_attr' => $statusAttribute->getBackendTable()], - "status_global_attr.{$linkField} = le.{$linkField}" - . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, - [] - )->joinLeft( - ['status_attr' => $statusAttribute->getBackendTable()], - "status_attr.{$linkField} = le.{$linkField}" - . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), - [] - )->where( - 'IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED - )->group( - ['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id'] + 'le.' . $linkField . ' = l.parent_id', + ['parent_id' => 'entity_id'] ); - $priceColumn = $this->_addAttributeToSelect($select, 'price', 'le.' . $linkField, 0, null, true); - $tierPriceColumn = $connection->getIfNullSql('MIN(i.tier_price)', 'NULL'); - $select->columns( - ['price' => $priceColumn, 'tier_price' => $tierPriceColumn] - ); + $select = $connection->select(); + $select->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '') + ->columns([ + 'sub.parent_id', + 'sub.entity_id', + 'sub.customer_group_id', + 'sub.website_id', + 'sub.price', + 'sub.tier_price', + ]); $query = $select->insertFromSelect($coaTable); $connection->query($query); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php new file mode 100644 index 0000000000000..ba0e9bba43ab9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php @@ -0,0 +1,10 @@ +get(Processor::class); +$indexerProcessor->getIndexer()->setScheduled(true); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php new file mode 100644 index 0000000000000..864226ea6c5d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php @@ -0,0 +1,10 @@ +get(Processor::class); +$indexerProcessor->getIndexer()->setScheduled(false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php new file mode 100644 index 0000000000000..e4e9daa7f2a88 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php @@ -0,0 +1,101 @@ +productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + $this->indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + } + + /** + * Use collection to check data in index. + * Do not use magentoDbIsolation because index statement changing "tears" transaction (triggers creating). + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/Catalog/_files/enable_price_index_schedule.php + */ + public function testFullReindexIfChildHasSpecialPrice() + { + $specialPrice = 2; + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals(10, $items[0]->getData('min_price')); + + $this->indexerProcessor->reindexAll(); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product $item */ + $item = $collection->getFirstItem(); + self::assertEquals($specialPrice, $item->getData('min_price')); + } + + /** + * Use collection to check data in index. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testOnSaveIndexationIfChildHasSpecialPrice() + { + $specialPrice = 2; + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product $item */ + $item = $collection->getFirstItem(); + self::assertEquals($specialPrice, $item->getData('min_price')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php new file mode 100644 index 0000000000000..60fd7e6403abc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php @@ -0,0 +1,121 @@ +productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + /** + * Check final price in configurable with special price in his child. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testPriceInfoIfChildHasSpecialPrice() + { + $specialPrice = 2; + + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var Product $configurableProduct */ + $configurableProduct = $this->productRepository->get('configurable', true); + $priceInfo = $configurableProduct->getPriceInfo(); + /** @var FinalPrice $finalPrice */ + $finalPrice = $priceInfo->getPrice(FinalPrice::PRICE_CODE); + + self::assertEquals($specialPrice, $finalPrice->getMinimalPrice()->getValue()); + } + + /** + * Check sorting configurable product without special price in his children. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_simple_77.php + */ + public function testSortingOfProductsIfChildHasNotSpecialPrice() + { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get('simple_77', true); + $simpleProduct + ->setOptions([]) + ->setTierPrice([]) + ->setPrice(5); + $this->productRepository->save($simpleProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->setVisibility([Visibility::VISIBILITY_IN_CATALOG, Visibility::VISIBILITY_BOTH]) + ->setOrder(ProductInterface::PRICE, Collection::SORT_ORDER_DESC); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals('configurable', $items[0]->getSku()); + self::assertEquals('simple_77', $items[1]->getSku()); + } + + /** + * Check sorting configurable product with special price in his child. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_simple_77.php + */ + public function testSortingOfProductsIfChildHasSpecialPrice() + { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get('simple_77', true); + $simpleProduct->setOptions([]) + ->setTierPrice([]) + ->setPrice(5); + $this->productRepository->save($simpleProduct); + + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', 2); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->setVisibility([Visibility::VISIBILITY_IN_CATALOG, Visibility::VISIBILITY_BOTH]) + ->setOrder(ProductInterface::PRICE, Collection::SORT_ORDER_DESC); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals('simple_77', $items[0]->getSku()); + self::assertEquals('configurable', $items[1]->getSku()); + } +} From bb65d05d41f30cd6a10fdfffb10b1dd8f42d3a77 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Thu, 4 May 2017 11:09:12 +0300 Subject: [PATCH 021/363] MAGETWO-55361: Portdown MAGETWO-45339 down to M2.1.x branch --- .../Model/Rule/Condition/Address.php | 1 - .../Unit/Model/Rule/Condition/AddressTest.php | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index 2b0da40a3e758..eddc689b286cf 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -61,7 +61,6 @@ public function loadAttributeOptions() 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), - 'payment_method' => __('Payment Method'), 'shipping_method' => __('Shipping Method'), 'postcode' => __('Shipping Postcode'), 'region' => __('Shipping Region'), diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php new file mode 100644 index 0000000000000..3219f5d769bbb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php @@ -0,0 +1,40 @@ + __('Subtotal'), + 'total_qty' => __('Total Items Quantity'), + 'weight' => __('Total Weight'), + 'shipping_method' => __('Shipping Method'), + 'postcode' => __('Shipping Postcode'), + 'region' => __('Shipping Region'), + 'region_id' => __('Shipping State/Province'), + 'country_id' => __('Shipping Country'), + ]; + + $objectManager = new ObjectManager($this); + + $this->address = $objectManager->getObject( + \Magento\SalesRule\Model\Rule\Condition\Address::class + ); + $this->address->loadAttributeOptions(); + + $this->assertEquals($attributes, $this->address->getAttributeOption()); + } +} From 3dded902177d8de70276d14fe3f1a655c2900a08 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Thu, 4 May 2017 11:16:13 +0300 Subject: [PATCH 022/363] MAGETWO-61059: [Backport] - Incorrect URLs in sitemap when generated from admin with 'Use Secure URLs in Admin' = Yes - for 2.1 --- app/code/Magento/Sitemap/Model/Sitemap.php | 9 +++- .../Sitemap/Test/Unit/Model/SitemapTest.php | 54 +++++++++++++------ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 3b6f646f9a547..5601031dd502b 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -201,7 +201,7 @@ public function __construct( */ protected function _construct() { - $this->_init('Magento\Sitemap\Model\ResourceModel\Sitemap'); + $this->_init(\Magento\Sitemap\Model\ResourceModel\Sitemap::class); } /** @@ -586,7 +586,12 @@ protected function _getBaseDir() */ protected function _getStoreBaseUrl($type = \Magento\Framework\UrlInterface::URL_TYPE_LINK) { - return rtrim($this->_storeManager->getStore($this->getStoreId())->getBaseUrl($type), '/') . '/'; + /** @var \Magento\Store\Model\Store $store */ + $store = $this->_storeManager->getStore($this->getStoreId()); + + $isSecure = $store->isUrlSecure(); + + return rtrim($store->getBaseUrl($type, $isSecure), '/') . '/'; } /** diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index a80c44f865f77..65b55f3e8dc87 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -50,22 +50,27 @@ class SitemapTest extends \PHPUnit_Framework_TestCase */ protected $_fileMock; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + /** * Set helper mocks, create resource model mock */ protected function setUp() { $this->_sitemapCategoryMock = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Catalog\Category' + \Magento\Sitemap\Model\ResourceModel\Catalog\Category::class )->disableOriginalConstructor()->getMock(); $this->_sitemapProductMock = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Catalog\Product' + \Magento\Sitemap\Model\ResourceModel\Catalog\Product::class )->disableOriginalConstructor()->getMock(); $this->_sitemapCmsPageMock = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Cms\Page' + \Magento\Sitemap\Model\ResourceModel\Cms\Page::class )->disableOriginalConstructor()->getMock(); $this->_helperMockSitemap = $this->getMock( - 'Magento\Sitemap\Helper\Data', + \Magento\Sitemap\Helper\Data::class, [ 'getCategoryChangefreq', 'getProductChangefreq', @@ -114,23 +119,23 @@ protected function setUp() $this->_helperMockSitemap->expects($this->any())->method('getPagePriority')->will($this->returnValue('0.25')); $this->_resourceMock = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Sitemap' + \Magento\Sitemap\Model\ResourceModel\Sitemap::class )->setMethods( ['_construct', 'beginTransaction', 'rollBack', 'save', 'addCommitCallback', 'commit', '__wakeup'] )->disableOriginalConstructor()->getMock(); $this->_resourceMock->expects($this->any())->method('addCommitCallback')->will($this->returnSelf()); $this->_fileMock = $this->getMockBuilder( - 'Magento\Framework\Filesystem\File\Write' + \Magento\Framework\Filesystem\File\Write::class )->disableOriginalConstructor()->getMock(); $this->_directoryMock = $this->getMockBuilder( - 'Magento\Framework\Filesystem\Directory\Write' + \Magento\Framework\Filesystem\Directory\Write::class )->disableOriginalConstructor()->getMock(); $this->_directoryMock->expects($this->any())->method('openFile')->will($this->returnValue($this->_fileMock)); $this->_filesystemMock = $this->getMockBuilder( - 'Magento\Framework\Filesystem' + \Magento\Framework\Filesystem::class )->setMethods( ['getDirectoryWrite'] )->disableOriginalConstructor()->getMock(); @@ -473,6 +478,20 @@ function ($from, $to) { $model = $this->_getModelMock(true); + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['isFrontUrlSecure', 'getBaseUrl']) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->expects($this->atLeastOnce())->method('isFrontUrlSecure')->willReturn(false); + $storeMock->expects($this->atLeastOnce()) + ->method('getBaseUrl') + ->with($this->isType('string'), false) + ->willReturn('http://store.com/'); + $this->storeManagerMock->expects($this->atLeastOnce()) + ->method('getStore') + ->with(1) + ->willReturn($storeMock); + return $model; } @@ -490,7 +509,6 @@ protected function _getModelMock($mockBeforeSave = false) '_getBaseDir', '_getFileObject', '_afterSave', - '_getStoreBaseUrl', '_getCurrentDateTime', '_getCategoryItemsCollection', '_getProductItemsCollection', @@ -554,7 +572,7 @@ protected function _getModelMock($mockBeforeSave = false) /** @var $model \Magento\Sitemap\Model\Sitemap */ $model = $this->getMockBuilder( - 'Magento\Sitemap\Model\Sitemap' + \Magento\Sitemap\Model\Sitemap::class )->setMethods( $methods )->setConstructorArgs( @@ -562,7 +580,6 @@ protected function _getModelMock($mockBeforeSave = false) )->getMock(); $model->expects($this->any())->method('_getResource')->will($this->returnValue($this->_resourceMock)); - $model->expects($this->any())->method('_getStoreBaseUrl')->will($this->returnValue('http://store.com/')); $model->expects( $this->any() )->method( @@ -585,7 +602,7 @@ protected function _getModelMock($mockBeforeSave = false) protected function _getModelConstructorArgs() { $categoryFactory = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Catalog\CategoryFactory' + \Magento\Sitemap\Model\ResourceModel\Catalog\CategoryFactory::class )->setMethods( ['create'] )->disableOriginalConstructor()->getMock(); @@ -598,26 +615,31 @@ protected function _getModelConstructorArgs() ); $productFactory = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Catalog\ProductFactory' + \Magento\Sitemap\Model\ResourceModel\Catalog\ProductFactory::class )->setMethods( ['create'] )->disableOriginalConstructor()->getMock(); $productFactory->expects($this->any())->method('create')->will($this->returnValue($this->_sitemapProductMock)); $cmsFactory = $this->getMockBuilder( - 'Magento\Sitemap\Model\ResourceModel\Cms\PageFactory' + \Magento\Sitemap\Model\ResourceModel\Cms\PageFactory::class )->setMethods( ['create'] )->disableOriginalConstructor()->getMock(); $cmsFactory->expects($this->any())->method('create')->will($this->returnValue($this->_sitemapCmsPageMock)); + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $constructArguments = $objectManager->getConstructArguments( - 'Magento\Sitemap\Model\Sitemap', + \Magento\Sitemap\Model\Sitemap::class, [ 'categoryFactory' => $categoryFactory, 'productFactory' => $productFactory, 'cmsFactory' => $cmsFactory, + 'storeManager' => $this->storeManagerMock, 'sitemapData' => $this->_helperMockSitemap, 'filesystem' => $this->_filesystemMock ] @@ -641,7 +663,7 @@ public function testGetSitemapUrl($storeBaseUrl, $documentRoot, $baseDir, $sitem { /** @var $model \Magento\Sitemap\Model\Sitemap */ $model = $this->getMockBuilder( - 'Magento\Sitemap\Model\Sitemap' + \Magento\Sitemap\Model\Sitemap::class )->setMethods( ['_getStoreBaseUrl', '_getDocumentRoot', '_getBaseDir', '_construct'] )->setConstructorArgs( From f8fa8f1699f8cdcc24c93ac25d45110bd3c461f0 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Thu, 4 May 2017 11:24:31 +0300 Subject: [PATCH 023/363] MAGETWO-63454: [Backport] Wrong address template used for order e-mails - for 2.1 --- .../Sales/Model/Order/Address/Renderer.php | 1 + .../Unit/Model/Order/Address/RendererTest.php | 145 ++++++++++++++++++ .../Model/Order/Address/RendererTest.php | 106 +++++++++++++ .../Sales/_files/order_fixture_store.php | 73 +++++---- .../_files/order_fixture_store_rollback.php | 17 ++ .../Store/_files/core_fixturestore.php | 8 +- 6 files changed, 310 insertions(+), 40 deletions(-) create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store_rollback.php diff --git a/app/code/Magento/Sales/Model/Order/Address/Renderer.php b/app/code/Magento/Sales/Model/Order/Address/Renderer.php index 515461c1c4b2a..a8214b6eabfa9 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Renderer.php +++ b/app/code/Magento/Sales/Model/Order/Address/Renderer.php @@ -48,6 +48,7 @@ public function __construct( */ public function format(Address $address, $type) { + $this->addressConfig->setStore($address->getOrder()->getStoreId()); $formatType = $this->addressConfig->getFormatByCode($type); if (!$formatType || !$formatType->getRenderer()) { return null; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php new file mode 100644 index 0000000000000..b9c97e41d4f55 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php @@ -0,0 +1,145 @@ +customerAddressConfigMock = $this->getMockBuilder(CustomerAddressConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eventManagerMock = $this->getMockBuilder(EventManager::class) + ->getMockForAbstractClass(); + $this->orderAddressMock = $this->getMockBuilder(OrderAddress::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerAddressBlockRendererMock = $this->getMockBuilder(CustomerAddressBlockRenderer::class) + ->getMockForAbstractClass(); + + $this->orderAddressMock->expects(static::any()) + ->method('getOrder') + ->willReturn($this->orderMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->orderAddressRenderer = $this->objectManagerHelper->getObject( + OrderAddressRenderer::class, + [ + 'addressConfig' => $this->customerAddressConfigMock, + 'eventManager' => $this->eventManagerMock + ] + ); + } + + public function testFormat() + { + $type = 'html'; + $formatType = new DataObject(['renderer' => $this->customerAddressBlockRendererMock]); + $addressData = ['address', 'data']; + $result = 'result string'; + + $this->setStoreExpectations(1); + $this->customerAddressConfigMock->expects(static::atLeastOnce()) + ->method('getFormatByCode') + ->with($type) + ->willReturn($formatType); + $this->eventManagerMock->expects(static::once()) + ->method('dispatch') + ->with('customer_address_format', ['type' => $formatType, 'address' => $this->orderAddressMock]); + $this->orderAddressMock->expects(static::atLeastOnce()) + ->method('getData') + ->willReturn($addressData); + $this->customerAddressBlockRendererMock->expects(static::once()) + ->method('renderArray') + ->with($addressData, null) + ->willReturn($result); + + $this->assertEquals($result, $this->orderAddressRenderer->format($this->orderAddressMock, $type)); + } + + public function testFormatNoRenderer() + { + $type = 'html'; + + $this->setStoreExpectations(1); + $this->customerAddressConfigMock->expects(static::atLeastOnce()) + ->method('getFormatByCode') + ->with($type) + ->willReturn(null); + $this->eventManagerMock->expects(static::never()) + ->method('dispatch'); + + $this->assertEquals(null, $this->orderAddressRenderer->format($this->orderAddressMock, $type)); + } + + /** + * Set expectations for store + * + * @param string|int $storeId + * @return void + */ + private function setStoreExpectations($storeId) + { + $this->orderMock->expects(static::atLeastOnce()) + ->method('getStoreId') + ->willReturn($storeId); + $this->customerAddressConfigMock->expects(static::atLeastOnce()) + ->method('setStore') + ->with($storeId) + ->willReturnSelf(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php new file mode 100644 index 0000000000000..f0b968d474d12 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php @@ -0,0 +1,106 @@ +objectManager = Bootstrap::getObjectManager(); + $this->orderAddressRenderer = $this->objectManager->get(OrderAddressRenderer::class); + $this->configResourceModel = $this->objectManager->get(ConfigResourceModel::class); + $this->config = $this->objectManager->get(Config::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_fixture_store.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testFormat() + { + $addressTemplates = [ + 'text' => 'text_customized', + 'oneline' => 'oneline_customized', + 'html' => 'html_customized', + 'pdf' => 'pdf_customized' + ]; + + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $storeId = $store->load('fixturestore')->getStoreId(); + + $this->configResourceModel->saveConfig( + 'customer/address_templates/text', + $addressTemplates['text'], + 'stores', + $storeId + ); + $this->configResourceModel->saveConfig( + 'customer/address_templates/oneline', + $addressTemplates['oneline'], + 'stores', + $storeId + ); + $this->configResourceModel->saveConfig( + 'customer/address_templates/html', + $addressTemplates['html'], + 'stores', + $storeId + ); + $this->configResourceModel->saveConfig( + 'customer/address_templates/pdf', + $addressTemplates['pdf'], + 'stores', + $storeId + ); + $this->config->clean(); + + /** @var Order $order */ + $order = $this->objectManager->create(Order::class) + ->loadByIncrementId('100000004'); + + /** @var OrderAddress $address */ + $address = $order->getBillingAddress(); + + $this->assertEquals($addressTemplates['text'], $this->orderAddressRenderer->format($address, 'text')); + $this->assertEquals($addressTemplates['oneline'], $this->orderAddressRenderer->format($address, 'oneline')); + $this->assertEquals($addressTemplates['html'], $this->orderAddressRenderer->format($address, 'html')); + $this->assertEquals($addressTemplates['pdf'], $this->orderAddressRenderer->format($address, 'pdf')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store.php index 1a195b1433d58..c9d965f50ac39 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store.php @@ -4,55 +4,54 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Catalog\Model\Product; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Payment as OrderPayment; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order; +use Magento\Store\Model\StoreManagerInterface; -require __DIR__ . '/../../../Magento/Store/_files/core_fixturestore.php'; +require __DIR__ . '/../../Store/_files/core_fixturestore.php'; -require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_duplicated.php'; -/** @var \Magento\Catalog\Model\Product $product */ +$objectManager = BootstrapHelper::getObjectManager(); -$addressData = include __DIR__ . '/address_data.php'; +$objectManager->get(IndexerRegistry::class) + ->get(FulltextIndexer::INDEXER_ID) + ->reindexAll(); + +require __DIR__ . '/../../Catalog/_files/product_simple_duplicated.php'; +/** @var Product $product */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$addressData = include __DIR__ . '/address_data.php'; -$billingAddress = $objectManager->create('Magento\Sales\Model\Order\Address', ['data' => $addressData]); +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); $billingAddress->setAddressType('billing'); $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType('shipping'); -$payment = $objectManager->create('Magento\Sales\Model\Order\Payment'); +$payment = $objectManager->create(OrderPayment::class); $payment->setMethod('checkmo'); -/** @var \Magento\Sales\Model\Order\Item $orderItem */ -$orderItem = $objectManager->create('Magento\Sales\Model\Order\Item'); +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); $orderItem->setProductId($product->getId())->setQtyOrdered(2); -/** @var \Magento\Sales\Model\Order $order */ -$order = $objectManager->create('Magento\Sales\Model\Order'); -$order->setIncrementId( - '100000004' -)->setState( - \Magento\Sales\Model\Order::STATE_PROCESSING -)->setStatus( - $order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING) -)->setSubtotal( - 100 -)->setBaseSubtotal( - 100 -)->setCustomerIsGuest( - true -)->setCustomerEmail( - 'customer@null.com' -)->setBillingAddress( - $billingAddress -)->setShippingAddress( - $shippingAddress -)->setStoreId( - $objectManager->get('Magento\Store\Model\StoreManagerInterface')->getStore('fixturestore')->getId() -)->addItem( - $orderItem -)->setPayment( - $payment -); +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000004') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setBaseSubtotal(100) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore('fixturestore')->getId()) + ->addItem($orderItem) + ->setPayment($payment); $order->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store_rollback.php new file mode 100644 index 0000000000000..5de1400aa1608 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_fixture_store_rollback.php @@ -0,0 +1,17 @@ +get(IndexerRegistry::class) + ->get(FulltextIndexer::INDEXER_ID) + ->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/core_fixturestore.php b/dev/tests/integration/testsuite/Magento/Store/_files/core_fixturestore.php index bd2050e712496..c905657750711 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/core_fixturestore.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/core_fixturestore.php @@ -7,10 +7,10 @@ use Magento\TestFramework\Helper\Bootstrap; /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ -$storeManager = Bootstrap::getObjectManager()->get('Magento\Store\Model\StoreManagerInterface'); +$storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); /** @var \Magento\Store\Model\Store $store */ -$store = Bootstrap::getObjectManager()->create('Magento\Store\Model\Store'); +$store = Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); $storeCode = 'fixturestore'; if (!$store->load($storeCode)->getId()) { @@ -23,5 +23,7 @@ $store->save(); /* Refresh stores memory cache */ - Bootstrap::getObjectManager()->get('Magento\Store\Model\StoreManagerInterface')->reinitStores(); + Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); } + +//if test using this fixture relies on full text functionality it is required to explicitly perform re-indexation From aaf85037b4db866b04ceed183578110ce1403e2c Mon Sep 17 00:00:00 2001 From: RomanKis Date: Thu, 4 May 2017 11:29:47 +0300 Subject: [PATCH 024/363] MAGETWO-58760: [Backport][Github] Customer Import - Invalid data for insert #4291 - for 2.1 --- .../Magento/CustomerImportExport/Model/Import/Customer.php | 4 ---- .../Model/Import/_files/customers_to_import.csv | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 6acb7cd43b866..9fa4a8954d588 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -370,10 +370,6 @@ protected function _prepareDataForUpdate(array $rowData) // attribute values foreach (array_intersect_key($rowData, $this->_attributes) as $attributeCode => $value) { - if ($newCustomer && !strlen($value)) { - continue; - } - $attributeParameters = $this->_attributes[$attributeCode]; if ('select' == $attributeParameters['type']) { $value = isset($attributeParameters['options'][strtolower($value)]) diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_import.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_import.csv index 310babaa9b078..30a283ce0502f 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_import.csv +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_to_import.csv @@ -1,7 +1,7 @@ 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 -AnthonyANealy@magento.com,base,admin,,5/6/2012 15:53,Admin,1,1,0,,Anthony,Male,1,Nealy,A.,6a9c9bfb2ba88a6ad2a64e7402df44a763e0c48cd21d7af9e7e796cd4677ee28:RF,,,,0,,,1, -LoriBBanks@magento.com,admin,admin,,5/6/2012 15:59,Admin,3,3,0,,Lori,Female,1,Banks,B.,7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz,,,,0,,,0, +AnthonyANealy@magento.com,base,admin,,5/6/2012 15:53,Admin,1,1,0,5/6/2010,Anthony,Male,1,Nealy,A.,6a9c9bfb2ba88a6ad2a64e7402df44a763e0c48cd21d7af9e7e796cd4677ee28:RF,,,,0,,,1, +LoriBBanks@magento.com,admin,admin,,5/6/2012 15:59,Admin,3,3,0,5/6/2010,Lori,Female,1,Banks,B.,7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz,,,,0,,,0, CharlesTAlston@teleworm.us,base,admin,,5/6/2012 16:13,Admin,4,4,0,,Jhon,Female,1,Doe,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, 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, +david.lamar@example.com,base,admin,,5/6/2012 16:25,Admin,4,4,0,,David,,1,Lamar,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, From ae8d9dd434ed66b0e4c85ed23fe2f1ef3a8c3265 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 4 May 2017 11:38:26 +0300 Subject: [PATCH 025/363] MAGETWO-60777: [Backport] - [Magento Cloud] - Static files not being generated correctly on prod(Race condition in merging files) - for 2.1 --- .../Framework/Filesystem/Directory/Write.php | 2 +- .../Test/Unit/Directory/WriteTest.php | 69 ++++++++++++++++++- .../View/Asset/MergeStrategy/Direct.php | 7 +- .../Unit/Asset/MergeStrategy/DirectTest.php | 42 +++++++---- 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 004ebd4d4f4e7..279f4476b5f9d 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -106,7 +106,7 @@ public function renameFile($path, $newPath, WriteInterface $targetDirectory = nu $targetDirectory->create($this->driver->getParentDirectory($newPath)); } $absolutePath = $this->driver->getAbsolutePath($this->path, $path); - $absoluteNewPath = $targetDirectory->driver->getAbsolutePath($this->path, $newPath); + $absoluteNewPath = $targetDirectory->getAbsolutePath($newPath); return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->driver); } diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php index 46cbfdb2077ad..d972efae15f38 100644 --- a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\Filesystem\Test\Unit\Directory; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; + class WriteTest extends \PHPUnit_Framework_TestCase { /** @@ -68,7 +71,7 @@ protected function tearDown() public function testGetDriver() { $this->assertInstanceOf( - \Magento\Framework\Filesystem\DriverInterface::class, + DriverInterface::class, $this->write->getDriver(), 'getDriver method expected to return instance of Magento\Framework\Filesystem\DriverInterface' ); @@ -90,7 +93,7 @@ public function testIsWritable() public function testCreateSymlinkTargetDirectoryExists() { - $targetDir = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\WriteInterface::class) + $targetDir = $this->getMockBuilder(WriteInterface::class) ->getMock(); $targetDir->driver = $this->driver; $sourcePath = 'source/path/file'; @@ -159,4 +162,66 @@ private function getAbsolutePath($path) { return $this->path . $path; } + + /** + * @param string $sourcePath + * @param string $targetPath + * @param WriteInterface $targetDir + * @dataProvider getFilePathsDataProvider + */ + public function testRenameFile($sourcePath, $targetPath, $targetDir) + { + if ($targetDir !== null) { + $targetDir->driver = $this->getMockBuilder(DriverInterface::class)->getMockForAbstractClass(); + $targetDirPath = 'TARGET_PATH/'; + $targetDir->expects($this->once()) + ->method('getAbsolutePath') + ->with($targetPath) + ->willReturn($targetDirPath . $targetPath); + $targetDir->expects($this->once()) + ->method('isExists') + ->with(dirname($targetPath)) + ->willReturn(false); + $targetDir->expects($this->once()) + ->method('create') + ->with(dirname($targetPath)); + } + + $this->driver->expects($this->any()) + ->method('getAbsolutePath') + ->willReturnMap([ + [$this->path, $sourcePath, null, $this->getAbsolutePath($sourcePath)], + [$this->path, $targetPath, null, $this->getAbsolutePath($targetPath)], + ]); + $this->driver->expects($this->any()) + ->method('isFile') + ->willReturnMap([ + [$this->getAbsolutePath($sourcePath), true], + [$this->getAbsolutePath($targetPath), true], + ]); + $this->driver->expects($this->any()) + ->method('getParentDirectory') + ->with($targetPath) + ->willReturn(dirname($targetPath)); + $this->write->renameFile($sourcePath, $targetPath, $targetDir); + } + + /** + * @return array + */ + public function getFilePathsDataProvider() + { + return [ + [ + 'path/to/source.file', + 'path/to/target.file', + null, + ], + [ + 'path/to/source.file', + 'path/to/target.file', + $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass(), + ], + ]; + } } diff --git a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php index 1e20e27ee538a..de9248b8fe10a 100644 --- a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php +++ b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php @@ -50,8 +50,11 @@ public function __construct( public function merge(array $assetsToMerge, Asset\LocalInterface $resultAsset) { $mergedContent = $this->composeMergedContent($assetsToMerge, $resultAsset); - $dir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); - $dir->writeFile($resultAsset->getPath(), $mergedContent); + $filePath = $resultAsset->getPath(); + $staticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); + $tmpDir = $this->filesystem->getDirectoryWrite(DirectoryList::TMP); + $tmpDir->writeFile($filePath, $mergedContent); + $tmpDir->renameFile($filePath, $filePath, $staticDir); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php index 510c42ca59626..553e7a0229008 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php @@ -5,8 +5,8 @@ */ namespace Magento\Framework\View\Test\Unit\Asset\MergeStrategy; -use \Magento\Framework\View\Asset\MergeStrategy\Direct; - +use Magento\Framework\View\Asset\MergeStrategy\Direct; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\App\Filesystem\DirectoryList; class DirectTest extends \PHPUnit_Framework_TestCase @@ -22,9 +22,14 @@ class DirectTest extends \PHPUnit_Framework_TestCase protected $cssUrlResolver; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Filesystem\Directory\WriteInterface + * @var \PHPUnit_Framework_MockObject_MockObject|WriteInterface + */ + protected $staticDir; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|WriteInterface */ - protected $writeDir; + protected $tmpDir; /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\View\Asset\LocalInterface @@ -33,21 +38,26 @@ class DirectTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->cssUrlResolver = $this->getMock('\Magento\Framework\View\Url\CssResolver'); - $filesystem = $this->getMock('\Magento\Framework\Filesystem', [], [], '', false); - $this->writeDir = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\WriteInterface'); + $this->cssUrlResolver = $this->getMock(\Magento\Framework\View\Url\CssResolver::class); + $filesystem = $this->getMock(\Magento\Framework\Filesystem::class, [], [], '', false); + $this->staticDir = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass(); + $this->tmpDir = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass(); $filesystem->expects($this->any()) ->method('getDirectoryWrite') - ->with(DirectoryList::STATIC_VIEW) - ->will($this->returnValue($this->writeDir)); - $this->resultAsset = $this->getMock('\Magento\Framework\View\Asset\File', [], [], '', false); + ->willReturnMap([ + [DirectoryList::STATIC_VIEW, \Magento\Framework\Filesystem\DriverPool::FILE, $this->staticDir], + [DirectoryList::TMP, \Magento\Framework\Filesystem\DriverPool::FILE, $this->tmpDir], + ]); + $this->resultAsset = $this->getMock(\Magento\Framework\View\Asset\File::class, [], [], '', false); $this->object = new Direct($filesystem, $this->cssUrlResolver); } public function testMergeNoAssets() { $this->resultAsset->expects($this->once())->method('getPath')->will($this->returnValue('foo/result')); - $this->writeDir->expects($this->once())->method('writeFile')->with('foo/result', ''); + $this->staticDir->expects($this->never())->method('writeFile'); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', ''); + $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); $this->object->merge([], $this->resultAsset); } @@ -55,7 +65,9 @@ public function testMergeGeneric() { $this->resultAsset->expects($this->once())->method('getPath')->will($this->returnValue('foo/result')); $assets = $this->prepareAssetsToMerge([' one', 'two']); // note leading space intentionally - $this->writeDir->expects($this->once())->method('writeFile')->with('foo/result', 'onetwo'); + $this->staticDir->expects($this->never())->method('writeFile'); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', 'onetwo'); + $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); $this->object->merge($assets, $this->resultAsset); } @@ -73,7 +85,9 @@ public function testMergeCss() ->method('aggregateImportDirectives') ->with('12') ->will($this->returnValue('1020')); - $this->writeDir->expects($this->once())->method('writeFile')->with('foo/result', '1020'); + $this->staticDir->expects($this->never())->method('writeFile'); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', '1020'); + $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); $this->object->merge($assets, $this->resultAsset); } @@ -87,7 +101,7 @@ private function prepareAssetsToMerge(array $data) { $result = []; foreach ($data as $content) { - $asset = $this->getMockForAbstractClass('Magento\Framework\View\Asset\LocalInterface'); + $asset = $this->getMockForAbstractClass(\Magento\Framework\View\Asset\LocalInterface::class); $asset->expects($this->once())->method('getContent')->will($this->returnValue($content)); $result[] = $asset; } From 9d5c048c5edc8b4049ac837558639940e11e89a9 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Thu, 4 May 2017 12:21:49 +0300 Subject: [PATCH 026/363] MAGETWO-63454: [Backport] Wrong address template used for order e-mails - for 2.1 --- .../Sales/Test/Unit/Model/Order/Address/RendererTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php index b9c97e41d4f55..c52e99aa2b6bb 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/RendererTest.php @@ -1,6 +1,6 @@ Date: Thu, 4 May 2017 12:36:07 +0300 Subject: [PATCH 027/363] MAGETWO-63124: [Backport] - Incorrect scope filter caching in UI grids - for 2.1 --- .../ui_component/product_listing.xml | 3 + .../Ui/view/base/web/js/grid/data-storage.js | 41 +++++++- .../Adminhtml/Product/FormPageActions.php | 43 ++++++++ .../Test/Block/Adminhtml/Product/Grid.php | 4 + .../Block/Adminhtml/Product/ProductForm.xml | 4 + .../AssertProductGridFilterCorrect.php | 99 +++++++++++++++++++ .../Test/Fixture/CatalogProductSimple.xml | 1 + .../Product/UpdateSimpleProductEntityTest.php | 28 +++++- .../Product/UpdateSimpleProductEntityTest.xml | 8 ++ .../Magento/Store/Test/Repository/Store.xml | 8 ++ .../Ui/base/js/grid/data-storage.test.js | 77 +++++++++++++++ 11 files changed, 308 insertions(+), 8 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index f7475d0bb8168..c2aa0b8c69622 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -23,6 +23,9 @@ Magento_Ui/js/grid/provider + + filters.store_id + diff --git a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js index 950e87b2bfb58..642c9b6072745 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js @@ -20,6 +20,7 @@ define([ method: 'GET', dataType: 'json' }, + dataScope: '', data: {} }, @@ -29,8 +30,16 @@ define([ * @returns {DataStorage} Chainable. */ initConfig: function () { + var scope; + this._super(); + scope = this.dataScope; + + if (typeof scope === 'string') { + this.dataScope = scope ? [scope] : []; + } + this._requests = []; return this; @@ -77,10 +86,12 @@ define([ * @returns {jQueryPromise} */ getData: function (params, options) { - var cachedRequest = this.getRequest(params); + var cachedRequest; - if (params && params.filters && params.filters['store_id']) { - cachedRequest = false; + if (this.hasScopeChanged(params)) { + this.clearRequests(); + } else { + cachedRequest = this.getRequest(params); } options = options || {}; @@ -90,6 +101,30 @@ define([ this.requestData(params); }, + /** + * Tells whether one of the parameters defined in the "dataScope" has + * changed since the last request. + * + * @param {Object} params - Request parameters. + * @returns {Boolean} + */ + hasScopeChanged: function (params) { + var lastRequest = _.last(this._requests), + keys, + diff; + + if (!lastRequest) { + return false; + } + + diff = utils.compare(lastRequest.params, params); + + keys = _.pluck(diff.changes, 'path'); + keys = keys.concat(Object.keys(diff.containers)); + + return _.intersection(this.dataScope, keys).length > 0; + }, + /** * Extends records of current data object * with the provided records collection. diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php index 9935d12739068..c7e195f7700de 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php @@ -58,6 +58,27 @@ class FormPageActions extends ParentFormPageActions */ private $addAttribute = '[data-ui-id="addattribute-button"]'; + /** + * Default store switcher block locator. + * + * @var string + */ + private $storeSwitcherBlock = '.store-switcher'; + + /** + * Selector for confirm. + * + * @var string + */ + private $confirmModal = '.confirm._show[data-role=modal]'; + + /** + * Dropdown block locator. + * + * @var string + */ + private $dropdownBlock = '.dropdown'; + /** * Click on "Save" button. * @@ -122,4 +143,26 @@ public function addNewAttribute() { $this->_rootElement->find($this->addAttribute)->click(); } + + /** + * Change Store View scope. + * + * @param FixtureInterface $store + * @return void + */ + public function changeStoreViewScope(FixtureInterface $store) + { + $this->waitForElementNotVisible($this->spinner); + $this->waitForElementVisible($this->storeSwitcherBlock); + $this->_rootElement->find($this->storeSwitcherBlock) + ->find($this->dropdownBlock, Locator::SELECTOR_CSS, 'liselectstore') + ->setValue(sprintf('%s/%s', $store->getGroupId(), $store->getName())); + $modalElement = $this->browser->find($this->confirmModal); + /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ + $modal = $this->blockFactory->create( + \Magento\Ui\Test\Block\Adminhtml\Modal::class, + ['element' => $modalElement] + ); + $modal->acceptAlert(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Grid.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Grid.php index 340e6f397bac3..186bbecab1e6f 100755 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Grid.php @@ -60,6 +60,10 @@ class Grid extends DataGrid 'selector' => '[name="attribute_set_id"]', 'input' => 'select', ], + 'store_id' => [ + 'selector' => '[name="store_id"]', + 'input' => 'select', + ], ]; /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml index 5f76393a1ec3c..02254f21c8e78 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml @@ -50,6 +50,10 @@ [name="product[news_to_date]"] css selector + + [name="use_default[name]"] + checkbox + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php new file mode 100644 index 0000000000000..caf5dc6ce983c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php @@ -0,0 +1,99 @@ +getId(); + //open products grid + $productIndex->open(); + /** @var Grid $productGrid */ + $productGrid = $productIndex->getProductGrid(); + $productGrid->resetFilter(); + + //form data for filters + $dataForFilterAllStores = [ + [ + 'name' => $initialProduct->getName(), + 'store_view' => self::ALL_STORE_VIEWS_FILTER + ] + ]; + + $dataForFilterCustomStores = []; + if (!empty($productNames) && !empty($stores)) { + foreach ($stores as $store) { + $storeName = $store->getName(); + $storeId = $store->getStoreId(); + if (isset($productNames[$storeId])) { + $dataForFilterCustomStores[] = [ + 'name' => $productNames[$storeId], + 'store_view' => $storeName + ]; + } + } + } + + $dataForFilters = array_merge($dataForFilterCustomStores, $dataForFilterAllStores, $dataForFilterCustomStores); + + //apply filters and compare results + foreach ($dataForFilters as $filterData) { + if (!empty($filterData)) { + $productGrid->sortByColumn('ID'); + $filter = [ + 'store_id' => $filterData['store_view'], + ]; + $productGrid->search($filter); + $gridProductName = $productGrid->getColumnValue($productId, 'Name'); + + $productGrid->sortByColumn('ID'); + + \PHPUnit_Framework_Assert::assertEquals( + $filterData['name'], + $gridProductName, + 'Product \'' . $initialProduct->getName() . '\' is absent in Products grid.' + ); + + } else { + $productGrid->resetFilter(); + } + } + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Products grid filter is cached correct'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml index fb34a7687c7e1..2fd1f97140ed4 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml @@ -47,6 +47,7 @@ + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php index eb996253d6276..5790220b8fbc3 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php @@ -9,8 +9,8 @@ use Magento\Catalog\Test\Fixture\CatalogProductSimple; use Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit; use Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex; -use Magento\Mtf\ObjectManager; use Magento\Mtf\TestCase\Injectable; +use Magento\Store\Test\Fixture\Store; /** * Precondition: @@ -77,11 +77,17 @@ public function __inject( * * @param CatalogProductSimple $initialProduct * @param CatalogProductSimple $product + * @param Store|null $store * @param string $configData * @return array + * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function test(CatalogProductSimple $initialProduct, CatalogProductSimple $product, $configData = '') - { + public function test( + CatalogProductSimple $initialProduct, + CatalogProductSimple $product, + Store $store = null, + $configData = '' + ) { $this->configData = $configData; // Preconditions $initialProduct->persist(); @@ -92,8 +98,13 @@ public function test(CatalogProductSimple $initialProduct, CatalogProductSimple ? $product->getDataFieldConfig('category_ids')['source']->getCategories()[0] : $initialCategory; + if ($store) { + $store->persist(); + $productName[$store->getStoreId()] = $product->getName(); + } + $this->objectManager->create( - 'Magento\Config\Test\TestStep\SetupConfigurationStep', + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $configData] )->run(); @@ -102,10 +113,17 @@ public function test(CatalogProductSimple $initialProduct, CatalogProductSimple $this->productGrid->open(); $this->productGrid->getProductGrid()->searchAndOpen($filter); + if ($store) { + $this->editProductPage->getFormPageActions()->changeStoreViewScope($store); + } $this->editProductPage->getProductForm()->fill($product); $this->editProductPage->getFormPageActions()->save(); - return ['category' => $category]; + return [ + 'category' => $category, + 'stores' => isset($store) ? [$store] : [], + 'productNames' => isset($productName) ? $productName : [], + ]; } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml index d4ec414088f28..1c574a888c792 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml @@ -146,5 +146,13 @@ + + product_with_category + custom_store_main_website + No + Test simple product %isolation% + + + diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Repository/Store.xml b/dev/tests/functional/tests/app/Magento/Store/Test/Repository/Store.xml index 5f34fae0ed1a3..62d965726c2a3 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Repository/Store.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Repository/Store.xml @@ -52,5 +52,13 @@ store_%isolation% Enabled + + + custom + + Store_%isolation% + store_%isolation% + Enabled + diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js new file mode 100644 index 0000000000000..ca25b8d3bf059 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js @@ -0,0 +1,77 @@ +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +/*jscs:disable requireCamelCaseOrUpperCaseIdentifiers*/ + +define([ + 'mageUtils', + 'Magento_Ui/js/grid/data-storage' +], function (utils, DataStorage) { + 'use strict'; + + describe('Magento_Ui/js/grid/data-storage', function () { + describe('costructor', function () { + it('converts dataScope property to array', function () { + var model = new DataStorage({ + dataScope: 'magento' + }); + + expect(model.dataScope).toEqual(['magento']); + }); + }); + + describe('hasScopeChanged', function () { + it('is function', function () { + var model = new DataStorage({ + dataScope: '' + }); + + expect(model.hasScopeChanged).toBeDefined(); + expect(typeof model.hasScopeChanged).toEqual('function'); + }); + + it('returns false if no requests have been made', function () { + var model = new DataStorage({ + dataScope: '' + }); + + expect(model.hasScopeChanged()).toBeFalsy(); + }); + + it('tells whether parameters defined in the dataScope property have changed', function () { + var params, newParams, model; + + params = { + namespace: 'magento', + search: '', + filters: { + store_id: 0 + }, + sorting: {}, + paging: {} + }; + + newParams = utils.extend({}, params, { + search: 'magento', + filters: { + store_id: 1 + } + }); + + model = new DataStorage({ + dataScope: 'filters.store_id' + }); + + model.cacheRequest({ + totalRecords: 0 + }, params); + + expect(model.hasScopeChanged(params)).toBeFalsy(); + expect(model.hasScopeChanged(newParams)).toBeTruthy(); + }); + }); + }); +}); From 9fcdeaa1f7da826179978e6301aa0cc9bd451255 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Thu, 4 May 2017 12:40:36 +0300 Subject: [PATCH 028/363] MAGETWO-63454: [Backport] Wrong address template used for order e-mails - for 2.1 --- .../Magento/Sales/Model/Order/Address/RendererTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php index f0b968d474d12..a70fabca860bb 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Address/RendererTest.php @@ -1,6 +1,6 @@ Date: Thu, 4 May 2017 13:02:56 +0300 Subject: [PATCH 029/363] MAGETWO-60742: TEST ISSUE: Failed Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest - Variation3 --- .../Helper/Plugin/Configurable.php | 2 +- .../Helper/Plugin/ConfigurableTest.php | 43 ++++++++++++++++--- .../AssertConfigurableProductForm.php | 1 + .../AssertConfigurableProductPage.php | 3 ++ .../ConfigurableAttributesData.php | 4 +- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php index d1017b8d6eb73..58c6dfa2809f6 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php @@ -95,7 +95,7 @@ public function afterInitialize(Helper $subject, ProductInterface $product) $product->setAttributeSetId($setId); } $extensionAttributes = $product->getExtensionAttributes(); - + $product->getResource()->getSortedAttributes($setId); $product->setNewVariationsAttributeSetId($setId); $configurableOptions = []; diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php index fe549f600ae9c..05837399588c5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php @@ -7,6 +7,7 @@ use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\ConfigurableProduct\Controller\Adminhtml\Product\Initialization\Helper\Plugin\Configurable; use Magento\ConfigurableProduct\Helper\Product\Options\Factory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProduct; @@ -74,7 +75,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods([ 'getTypeId', 'setAttributeSetId', 'getExtensionAttributes', 'setNewVariationsAttributeSetId', - 'setCanSaveConfigurableAttributes', 'setExtensionAttributes', 'hasData', 'getData' + 'setCanSaveConfigurableAttributes', 'setExtensionAttributes', 'hasData', 'getData', 'getResource' ]) ->getMock(); @@ -94,6 +95,9 @@ protected function setUp() */ public function testAfterInitializeWithAttributesAndVariations() { + $postValue = 24; + $productResourceMock = $this->getProductResource($postValue); + $attributes = [ ['attribute_id' => 90, 'values' => [ ['value_index' => 12], ['value_index' => 13] @@ -152,7 +156,7 @@ public function testAfterInitializeWithAttributesAndVariations() 'configurable-matrix' => $simpleProducts ]; $valueMap = [ - ['new-variations-attribute-set-id', null, 24], + ['new-variations-attribute-set-id', null, $postValue], ['product', [], $productData] ]; @@ -164,20 +168,24 @@ public function testAfterInitializeWithAttributesAndVariations() ->method('getTypeId') ->willReturn(ConfigurableProduct::TYPE_CODE); - $this->product->expects(static::at(4)) + $this->product->expects(static::at(5)) ->method('hasData') ->with('associated_product_ids') ->willReturn(false); - $this->product->expects(static::at(5)) + $this->product->expects(static::at(6)) ->method('hasData') ->with('configurable-matrix') ->willReturn(true); - $this->product->expects(static::at(6)) + $this->product->expects(static::at(7)) ->method('getData') ->with('configurable-matrix') ->willReturn($simpleProducts); + $this->product->expects(static::once()) + ->method('getResource') + ->willReturn($productResourceMock); + $this->request->expects(static::any()) ->method('getPost') ->willReturnMap($valueMap); @@ -225,6 +233,9 @@ public function testAfterInitializeWithAttributesAndVariations() public function testAfterInitializeWithAttributesAndWithoutVariations() { + $postValue = 24; + $productResourceMock = $this->getProductResource($postValue); + $attributes = [ ['attribute_id' => 90, 'values' => [ ['value_index' => 12], ['value_index' => 13] @@ -238,7 +249,7 @@ public function testAfterInitializeWithAttributesAndWithoutVariations() ]; $valueMap = [ - ['new-variations-attribute-set-id', null, 24], + ['new-variations-attribute-set-id', null, $postValue], ['product', [], $productData], ]; $paramValueMap = [ @@ -254,6 +265,9 @@ public function testAfterInitializeWithAttributesAndWithoutVariations() $this->product->expects(static::at(0)) ->method('getData') ->willReturn(ConfigurableProduct::TYPE_CODE); + $this->product->expects(static::once()) + ->method('getResource') + ->willReturn($productResourceMock); $this->request->expects(static::any()) ->method('getPost') @@ -331,4 +345,21 @@ public function testAfterInitializeForNotConfigurableProduct() ->method('generateSimpleProducts'); $this->plugin->afterInitialize($this->subject, $this->product); } + + /** + * generate product resource model mock + * @param $postValue + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getProductResource($postValue) + { + $productResourceMock = $this->getMockBuilder(ProductResource::class) + ->disableOriginalConstructor() + ->getMock(); + $productResourceMock->expects(static::once()) + ->method('getSortedAttributes') + ->with($postValue); + + return $productResourceMock; + } } diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductForm.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductForm.php index 5b4e61f08dbd7..58f8f06b428cc 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductForm.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductForm.php @@ -60,6 +60,7 @@ class AssertConfigurableProductForm extends AssertProductForm */ protected $skippedVariationMatrixFields = [ 'configurable_attribute', + 'special_price' ]; /** diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php index 096189562c125..9df251cd74bbf 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php @@ -129,6 +129,9 @@ protected function getLowestConfigurablePrice() if ($price > $option['price']) { $price = $option['price']; } + if (isset($option['special_price']) && $price > $option['special_price']) { + $price = $option['special_price']; + } } return $price; diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php index 496edcae960cb..b8298bb653116 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php @@ -292,7 +292,8 @@ protected function prepareVariationsMatrix(array $data) 'sku' => $product->getSku(), 'qty' => $quantityAndStockStatus['qty'], 'weight' => $product->getWeight(), - 'price' => $product->getPrice() + 'price' => $product->getPrice(), + 'special_price' => $product->getSpecialPrice() ]; $this->variationsMatrix[$key] = array_replace_recursive($this->variationsMatrix[$key], $productData); } else { @@ -381,6 +382,7 @@ protected function prepareData() 'price', 'qty', 'weight', + 'special_price' ]; $this->data = [ From 18aaf696f348e66c6b2a9ea0250a21d84e172219 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Thu, 4 May 2017 14:23:23 +0300 Subject: [PATCH 030/363] MAGETWO-62914: [Backport] - [Github] Directive values are not quote-escaped #3860 - for 2.1 --- .../Block/Product/Widget/Conditions.php | 12 +- .../Block/Product/Widget/ConditionsTest.php | 174 ++++++++++++++ .../Adminhtml/Widget/LoadOptions.php | 31 ++- app/code/Magento/Widget/Model/Widget.php | 5 +- .../Adminhtml/Widget/LoadOptionsTest.php | 219 ++++++++++++++++++ .../Widget/Test/Unit/Model/WidgetTest.php | 123 ++++++++-- 6 files changed, 538 insertions(+), 26 deletions(-) create mode 100644 app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php create mode 100644 app/code/Magento/Widget/Test/Unit/Controller/Adminhtml/Widget/LoadOptionsTest.php diff --git a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php index f1c075fd54924..2a852a708fa88 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php +++ b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php @@ -73,6 +73,7 @@ public function __construct( $this->conditions = $conditions; $this->rule = $rule; $this->registry = $registry; + parent::__construct($context, $data); } @@ -81,12 +82,17 @@ public function __construct( */ protected function _construct() { + $widgetParameters = []; $widget = $this->registry->registry('current_widget_instance'); + if ($widget) { $widgetParameters = $widget->getWidgetParameters(); - if (isset($widgetParameters['conditions'])) { - $this->rule->loadPost($widgetParameters); - } + } elseif (($widgetOptions = $this->getLayout()->getBlock('wysiwyg_widget.options')) != false) { + $widgetParameters = $widgetOptions->getWidgetValues(); + } + + if (isset($widgetParameters['conditions'])) { + $this->rule->loadPost($widgetParameters); } } diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php new file mode 100644 index 0000000000000..79a25d2f397bd --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php @@ -0,0 +1,174 @@ +objectManagerHelper = new ObjectManagerHelper($this); + $this->ruleMock = $this->getMockBuilder(Rule::class) + ->disableOriginalConstructor() + ->getMock(); + $this->registryMock = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->layoutMock = $this->getMockForAbstractClass(LayoutInterface::class); + $this->blockMock = $this->getMockBuilder(BlockInterface::class) + ->setMethods(['getWidgetValues']) + ->getMockForAbstractClass(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->once()) + ->method('getLayout') + ->willReturn($this->layoutMock); + } + + /** + * @return void + */ + public function testConstructWithEmptyData() + { + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn(null); + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('wysiwyg_widget.options') + ->willReturn(null); + $this->blockMock->expects($this->never()) + ->method('getWidgetValues'); + $this->ruleMock->expects($this->never()) + ->method('loadPost'); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } + + /** + * @return void + */ + public function testConstructWithWidgetInstance() + { + $widgetParams = ['conditions' => 'some conditions']; + + /** @var \Magento\Widget\Model\Widget\Instance|\PHPUnit_Framework_MockObject_MockObject $widgetMock */ + $widgetMock = $this->getMockBuilder(\Magento\Widget\Model\Widget\Instance::class) + ->disableOriginalConstructor() + ->getMock(); + $widgetMock->expects($this->once()) + ->method('getWidgetParameters') + ->willReturn($widgetParams); + + $this->layoutMock->expects($this->never()) + ->method('getBlock'); + $this->blockMock->expects($this->never()) + ->method('getWidgetValues'); + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn($widgetMock); + $this->ruleMock->expects($this->once()) + ->method('loadPost') + ->with($widgetParams) + ->willReturnSelf(); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } + + /** + * @return void + */ + public function testConstructWithParamsFromBlock() + { + $widgetParams = ['conditions' => 'some conditions']; + + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn(null); + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('wysiwyg_widget.options') + ->willReturn($this->blockMock); + $this->blockMock->expects($this->once()) + ->method('getWidgetValues') + ->willReturn($widgetParams); + $this->ruleMock->expects($this->once()) + ->method('loadPost') + ->with($widgetParams) + ->willReturnSelf(); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } +} diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php index 1088f55e9929d..2f78208bb6c92 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php @@ -1,13 +1,19 @@ _view->loadLayout(); if ($paramsJson = $this->getRequest()->getParam('widget')) { - $request = $this->_objectManager->get('Magento\Framework\Json\Helper\Data')->jsonDecode($paramsJson); + $request = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class) + ->jsonDecode($paramsJson); if (is_array($request)) { $optionsBlock = $this->_view->getLayout()->getBlock('wysiwyg_widget.options'); if (isset($request['widget_type'])) { $optionsBlock->setWidgetType($request['widget_type']); } if (isset($request['values'])) { + $request['values'] = array_map('htmlspecialchars_decode', $request['values']); + if (isset($request['values']['conditions_encoded'])) { + $request['values']['conditions'] = + $this->getConditionsHelper()->decode($request['values']['conditions_encoded']); + } $optionsBlock->setWidgetValues($request['values']); } } @@ -33,8 +45,21 @@ public function execute() } catch (\Magento\Framework\Exception\LocalizedException $e) { $result = ['error' => true, 'message' => $e->getMessage()]; $this->getResponse()->representJson( - $this->_objectManager->get('Magento\Framework\Json\Helper\Data')->jsonEncode($result) + $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result) ); } } + + /** + * @return \Magento\Widget\Helper\Conditions + * @deprecated + */ + private function getConditionsHelper() + { + if (!$this->conditionsHelper) { + $this->conditionsHelper = ObjectManager::getInstance()->get(\Magento\Widget\Helper\Conditions::class); + } + + return $this->conditionsHelper; + } } diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 21401fb4cdab6..99160aa40e58b 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -89,7 +89,7 @@ private function getMathRandom() { if ($this->mathRandom === null) { $this->mathRandom = \Magento\Framework\App\ObjectManager::getInstance() - ->get('\Magento\Framework\Math\Random'); + ->get(\Magento\Framework\Math\Random::class); } return $this->mathRandom; } @@ -314,12 +314,11 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) } } if ($value) { - $directive .= sprintf(' %s="%s"', $name, $value); + $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeQuote($value)); } } $directive .= $this->getWidgetPageVarName($params); - $directive .= '}}'; if ($asIs) { diff --git a/app/code/Magento/Widget/Test/Unit/Controller/Adminhtml/Widget/LoadOptionsTest.php b/app/code/Magento/Widget/Test/Unit/Controller/Adminhtml/Widget/LoadOptionsTest.php new file mode 100644 index 0000000000000..3862eb9668da0 --- /dev/null +++ b/app/code/Magento/Widget/Test/Unit/Controller/Adminhtml/Widget/LoadOptionsTest.php @@ -0,0 +1,219 @@ +objectManagerHelper = new ObjectManagerHelper($this); + $this->objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->viewMock = $this->getMockForAbstractClass(ViewInterface::class); + $this->requestMock = $this->getMockForAbstractClass(RequestInterface::class); + $this->responseMock = $this->getMockBuilder(ResponseInterface::class) + ->setMethods(['representJson']) + ->getMockForAbstractClass(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->once()) + ->method('getView') + ->willReturn($this->viewMock); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + $this->contextMock->expects($this->once()) + ->method('getObjectManager') + ->willReturn($this->objectManagerMock); + $this->conditionsHelperMock = $this->getMockBuilder(ConditionsHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->loadOptions = $this->objectManagerHelper->getObject( + LoadOptions::class, + ['context' => $this->contextMock] + ); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->loadOptions, + 'conditionsHelper', + $this->conditionsHelperMock + ); + } + + /** + * @return void + */ + public function dtestExecuteWithException() + { + $jsonResult = '{"error":true,"message":"Some error"}'; + $errorMessage = 'Some error'; + + /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject $jsonDataHelperMock */ + $jsonDataHelperMock = $this->getMockBuilder(\Magento\Framework\Json\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $jsonDataHelperMock->expects($this->once()) + ->method('jsonEncode') + ->with(['error' => true, 'message' => $errorMessage]) + ->willReturn($jsonResult); + + $this->viewMock->expects($this->once()) + ->method('loadLayout') + ->willThrowException(new LocalizedException(__($errorMessage))); + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->with(\Magento\Framework\Json\Helper\Data::class) + ->willReturn($jsonDataHelperMock); + $this->responseMock->expects($this->once()) + ->method('representJson') + ->with($jsonResult) + ->willReturnArgument(0); + + $this->loadOptions->execute(); + } + + /** + * @return void + */ + public function testExecute() + { + $widgetType = 'Magento\SomeWidget'; + $conditionsEncoded = 'a:3:{s:5:"value";i:1;s:8:"operator";s:2:"==";s:9:"attribute";s:2:"id";}'; + $conditionsDecoded = [ + 'value' => 1, + 'operator' => '==', + 'attribute' => 'id', + ]; + $widgetJsonParams = '{"widget_type":"Magento\\Widget","values":{"title":""Test"", "":}}'; + $widgetArrayParams = [ + 'widget_type' => $widgetType, + 'values' => [ + 'title' => '"Test"', + 'conditions_encoded' => $conditionsEncoded, + ], + ]; + $resultWidgetArrayParams = [ + 'widget_type' => $widgetType, + 'values' => [ + 'title' => '"Test"', + 'conditions_encoded' => $conditionsEncoded, + 'conditions' => $conditionsDecoded, + ], + ]; + + /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject $jsonDataHelperMock */ + $jsonDataHelperMock = $this->getMockBuilder(\Magento\Framework\Json\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $jsonDataHelperMock->expects($this->once()) + ->method('jsonDecode') + ->with($widgetJsonParams) + ->willReturn($widgetArrayParams); + + $this->viewMock->expects($this->once()) + ->method('loadLayout'); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('widget') + ->willReturn($widgetJsonParams); + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->with(\Magento\Framework\Json\Helper\Data::class) + ->willReturn($jsonDataHelperMock); + + /** @var \Magento\Framework\View\Element\BlockInterface|\PHPUnit_Framework_MockObject_MockObject $blockMock */ + $blockMock = $this->getMockBuilder(\Magento\Framework\View\Element\BlockInterface::class) + ->setMethods(['setWidgetType', 'setWidgetValues']) + ->getMockForAbstractClass(); + $blockMock->expects($this->once()) + ->method('setWidgetType') + ->with($widgetType) + ->willReturnSelf(); + $blockMock->expects($this->once()) + ->method('setWidgetValues') + ->with($resultWidgetArrayParams['values']) + ->willReturnSelf(); + + /** @var \Magento\Framework\View\LayoutInterface|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ + $layoutMock = $this->getMockForAbstractClass(\Magento\Framework\View\LayoutInterface::class); + $layoutMock->expects($this->once()) + ->method('getBlock') + ->with('wysiwyg_widget.options') + ->willReturn($blockMock); + + $this->conditionsHelperMock->expects($this->once()) + ->method('decode') + ->with($conditionsEncoded) + ->willReturn($conditionsDecoded); + $this->viewMock->expects($this->once()) + ->method('getLayout') + ->willReturn($layoutMock); + $this->viewMock->expects($this->once()) + ->method('renderLayout'); + + $this->loadOptions->execute(); + } +} diff --git a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php index 9daf8458bb841..cc25cc1aa1c93 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php @@ -5,6 +5,12 @@ */ namespace Magento\Widget\Test\Unit\Model; +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Test class for \Magento\Widget\Model\Widget + */ class WidgetTest extends \PHPUnit_Framework_TestCase { /** @@ -12,6 +18,16 @@ class WidgetTest extends \PHPUnit_Framework_TestCase */ protected $dataStorageMock; + /** + * @var ObjectManager + */ + protected $objectManagerHelper; + + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Widget\Model\Widget */ @@ -24,17 +40,24 @@ class WidgetTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->dataStorageMock = $this->getMockBuilder('Magento\Widget\Model\Config\Data') + $this->dataStorageMock = $this->getMockBuilder(\Magento\Widget\Model\Config\Data::class) ->disableOriginalConstructor() ->getMock(); - $this->conditionsHelper = $this->getMockBuilder('\Magento\Widget\Helper\Conditions') + $this->conditionsHelper = $this->getMockBuilder(\Magento\Widget\Helper\Conditions::class) ->setMethods(['encode']) ->disableOriginalConstructor() ->getMock(); - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->widget = $objectManagerHelper->getObject( - 'Magento\Widget\Model\Widget', - ['dataStorage' => $this->dataStorageMock, 'conditionsHelper' => $this->conditionsHelper] + $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper = new ObjectManager($this); + $this->widget = $this->objectManagerHelper->getObject( + \Magento\Widget\Model\Widget::class, + [ + 'dataStorage' => $this->dataStorageMock, + 'conditionsHelper' => $this->conditionsHelper, + 'escaper' => $this->escaperMock, + ] ); } @@ -92,8 +115,8 @@ public function testGetConfigAsObject() ->method('get') ->willReturn($widgets); - $resultObject = $this->widget->getConfigAsObject('Magento\Cms\Block\Widget\Page\Link'); - $this->assertInstanceOf('Magento\Framework\DataObject', $resultObject); + $resultObject = $this->widget->getConfigAsObject(\Magento\Cms\Block\Widget\Page\Link::class); + $this->assertInstanceOf(\Magento\Framework\DataObject::class, $resultObject); $this->assertSame('CMS Page Link', $resultObject->getName()); $this->assertSame('Link to a CMS Page', $resultObject->getDescription()); @@ -101,9 +124,9 @@ public function testGetConfigAsObject() $this->assertSame('Magento_Cms::images/widget_page_link.png', $resultObject->getPlaceholderImage()); $resultParameters = $resultObject->getParameters(); - $this->assertInstanceOf('Magento\Framework\DataObject', $resultParameters['page_id' ]); - $this->assertInstanceOf('Magento\Framework\DataObject', $resultParameters['anchor_text']); - $this->assertInstanceOf('Magento\Framework\DataObject', $resultParameters['template']); + $this->assertInstanceOf(\Magento\Framework\DataObject::class, $resultParameters['page_id']); + $this->assertInstanceOf(\Magento\Framework\DataObject::class, $resultParameters['anchor_text']); + $this->assertInstanceOf(\Magento\Framework\DataObject::class, $resultParameters['template']); $supportedContainersExpected = [ '0' => [ @@ -124,14 +147,14 @@ public function testGetConfigAsObjectWidgetNoFound() ->method('get') ->willReturn([]); - $resultObject = $this->widget->getConfigAsObject('Magento\Cms\Block\Widget\Page\Link'); - $this->assertInstanceOf('Magento\Framework\DataObject', $resultObject); + $resultObject = $this->widget->getConfigAsObject(\Magento\Cms\Block\Widget\Page\Link::class); + $this->assertInstanceOf(\Magento\Framework\DataObject::class, $resultObject); $this->assertSame([], $resultObject->getData()); } public function testGetWidgetDeclaration() { - $mathRandomMock = $this->getMock('\Magento\Framework\Math\Random', ['getRandomString'], [], '', false); + $mathRandomMock = $this->getMock(\Magento\Framework\Math\Random::class, ['getRandomString'], [], '', false); $mathRandomMock->expects($this->any())->method('getRandomString')->willReturn('asdf'); $reflection = new \ReflectionClass(get_class($this->widget)); $reflectionProperty = $reflection->getProperty('mathRandom'); @@ -140,14 +163,14 @@ public function testGetWidgetDeclaration() $conditions = [ [ - 'type' => 'Magento\CatalogWidget\Model\Rule\Condition\Combine', + 'type' => \Magento\CatalogWidget\Model\Rule\Condition\Combine::class, 'aggregator' => 'all', 'value' => '1', 'new_child' => '' ] ]; $params = [ - 'title' => 'my widget', + 'title' => 'my "widget"', 'show_pager' => '1', 'products_per_page' => '5', 'products_count' => '10', @@ -157,9 +180,75 @@ public function testGetWidgetDeclaration() $this->conditionsHelper->expects($this->once())->method('encode')->with($conditions) ->willReturn('encoded-conditions-string'); - $result = $this->widget->getWidgetDeclaration('Magento\CatalogWidget\Block\Product\ProductsList', $params); + $this->escaperMock->expects($this->atLeastOnce()) + ->method('escapeQuote') + ->willReturnMap([ + ['my "widget"', false, 'my "widget"'], + ['1', false, '1'], + ['5', false, '5'], + ['10', false, '10'], + ['product/widget/content/grid.phtml', false, 'product/widget/content/grid.phtml'], + ['encoded-conditions-string', false, 'encoded-conditions-string'], + ]); + + $result = $this->widget->getWidgetDeclaration(ProductsList::class, $params); + $this->assertContains('{{widget type="Magento\CatalogWidget\Block\Product\ProductsList"', $result); + $this->assertContains('title="my "widget""', $result); + $this->assertContains('conditions_encoded="encoded-conditions-string"', $result); + $this->assertContains('page_var_name="pasdf"}}', $result); + } + + public function testGetWidgetDeclarationWithZeroValueParam() + { + $mathRandomMock = $this->getMock(\Magento\Framework\Math\Random::class, ['getRandomString'], [], '', false); + $mathRandomMock->expects($this->any()) + ->method('getRandomString') + ->willReturn('asdf'); + + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->widget, + 'mathRandom', + $mathRandomMock + ); + + $conditions = [ + [ + 'type' => \Magento\CatalogWidget\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => '1', + 'new_child' => '' + ] + ]; + $params = [ + 'title' => 'my widget', + 'show_pager' => '1', + 'products_per_page' => '5', + 'products_count' => '0', + 'template' => 'product/widget/content/grid.phtml', + 'conditions' => $conditions + ]; + + $this->conditionsHelper->expects($this->once()) + ->method('encode') + ->with($conditions) + ->willReturn('encoded-conditions-string'); + $this->escaperMock->expects($this->atLeastOnce()) + ->method('escapeQuote') + ->willReturnMap([ + ['my "widget"', false, 'my "widget"'], + ['0', false, '0'], + ['1', false, '1'], + ['5', false, '5'], + ['10', false, '10'], + ['product/widget/content/grid.phtml', false, 'product/widget/content/grid.phtml'], + ['encoded-conditions-string', false, 'encoded-conditions-string'], + ]); + + $result = $this->widget->getWidgetDeclaration(ProductsList::class, $params); + $this->assertContains('{{widget type="' . ProductsList::class . '"', $result); $this->assertContains('conditions_encoded="encoded-conditions-string"', $result); $this->assertContains('page_var_name="pasdf"}}', $result); + $this->assertContains('products_count="0"', $result); } } From 19856c7b5f3a4ba18b23003dd34a72b668fca5b4 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 4 May 2017 14:23:41 +0300 Subject: [PATCH 031/363] MAGETWO-60742: TEST ISSUE: Failed Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest - Variation3 --- .../Test/TestCase/CreateConfigurableProductEntityTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml index a391ded27c18e..86cd87ffac1bf 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml @@ -54,7 +54,7 @@ - to_maintain:yes + to_maintain:no configurable-product-%isolation% two_options_with_assigned_product_special_price configurable_two_new_options_with_special_price From f727ca1a11260669d92b49100ab38b6b603ee734 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 4 May 2017 14:34:45 +0300 Subject: [PATCH 032/363] MAGETWO-62623: [Backport] - MAGETWO-62624: Order total still displays when no related option is selected --- .../Test/Block/Adminhtml/Order/Create.php | 2 +- .../Block/Adminhtml/Order/Create/Totals.php | 29 +++++++++++++++++++ .../InjectableTests/MAGETWO-62623.xml | 14 +++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php index dd749f672fd48..b934f8eaf2bfb 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php @@ -191,7 +191,7 @@ protected function getShippingMethodBlock() * * @return \Magento\Sales\Test\Block\Adminhtml\Order\Create\Totals */ - protected function getTotalsBlock() + public function getTotalsBlock() { return $this->blockFactory->create( \Magento\Sales\Test\Block\Adminhtml\Order\Create\Totals::class, diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Totals.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Totals.php index 818afeeb9e44a..079e3d663b4fb 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Totals.php @@ -7,6 +7,7 @@ namespace Magento\Sales\Test\Block\Adminhtml\Order\Create; use Magento\Mtf\Block\Block; +use Magento\Mtf\Client\Locator; /** * Class Totals @@ -22,6 +23,20 @@ class Totals extends Block */ protected $submitOrder = '.order-totals-actions button'; + /** + * Order totals table. + * + * @var string + */ + protected $totalsTable = '.data-table'; + + /** + * Total row label selector. + * + * @var string + */ + protected $totalLabelLocator = './/tr[normalize-space(td)="%s"]'; + /** * Click 'Submit Order' button */ @@ -29,4 +44,18 @@ public function submitOrder() { $this->_rootElement->find($this->submitOrder)->click(); } + + /** + * Return total presence by label. + * + * @param string $total + * @return bool + */ + public function isTotalPresent($total) + { + $totalsTable = $this->_rootElement->find($this->totalsTable); + $totalRow = $totalsTable->find(sprintf($this->totalLabelLocator, $total), Locator::SELECTOR_XPATH); + + return $totalRow->isVisible(); + } } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml new file mode 100644 index 0000000000000..dab7b4103db48 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml @@ -0,0 +1,14 @@ + + + + + + + + + From ebb22f8719aac4b215c9ba72a662dfe5c59a4810 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 4 May 2017 15:14:28 +0300 Subject: [PATCH 033/363] MAGETWO-62623: [Backport] - MAGETWO-62624: Order total still displays when no related option is selected --- .../TestSuite/InjectableTests/MAGETWO-62623.xml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml deleted file mode 100644 index dab7b4103db48..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-62623.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - From e9ff1c24fd1bab700dc363dde59f94176aab72ee Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Thu, 4 May 2017 16:03:33 +0300 Subject: [PATCH 034/363] MAGETWO-62914: [Backport] - [Github] Directive values are not quote-escaped #3860 - for 2.1 --- app/code/Magento/Widget/Model/Widget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 99160aa40e58b..e3c3f40ee010f 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -313,7 +313,7 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) $value = $parameters[$name]->getValue(); } } - if ($value) { + if (isset($value)) { $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeQuote($value)); } } From 3a3cf91a7d53a2579caff045bd22a983ace665b7 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Thu, 4 May 2017 17:09:54 +0300 Subject: [PATCH 035/363] MAGETWO-63124: [Backport] - Incorrect scope filter caching in UI grids - for 2.1 --- .../Constraint/AssertProductGridFilterCorrect.php | 14 ++++++-------- .../Product/UpdateSimpleProductEntityTest.php | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php index caf5dc6ce983c..7acea4a50f695 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php @@ -32,7 +32,7 @@ public function processAssert( array $productNames, array $stores ) { - $productId = $initialProduct->getId(); + $productSku = $initialProduct->getSku(); //open products grid $productIndex->open(); /** @var Grid $productGrid */ @@ -66,18 +66,16 @@ public function processAssert( //apply filters and compare results foreach ($dataForFilters as $filterData) { if (!empty($filterData)) { - $productGrid->sortByColumn('ID'); $filter = [ 'store_id' => $filterData['store_view'], + 'sku' => $productSku, ]; + $productGrid->resetFilter(); $productGrid->search($filter); - $gridProductName = $productGrid->getColumnValue($productId, 'Name'); - - $productGrid->sortByColumn('ID'); + $res = $productGrid->isRowVisible(['name' => $filterData['name']], false, true); - \PHPUnit_Framework_Assert::assertEquals( - $filterData['name'], - $gridProductName, + \PHPUnit_Framework_Assert::assertTrue( + $res, 'Product \'' . $initialProduct->getName() . '\' is absent in Products grid.' ); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php index 5790220b8fbc3..ee72b6ab3f62d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php @@ -135,7 +135,7 @@ public function tearDown() { if ($this->configData) { $this->objectManager->create( - 'Magento\Config\Test\TestStep\SetupConfigurationStep', + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $this->configData, 'rollback' => true] )->run(); } From 86b36f27153bc4478bc08216ee6218e906551632 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 4 May 2017 17:55:38 +0300 Subject: [PATCH 036/363] MAGETWO-55519: Portdown MAGETWO-53986 down to M2.1.x branch --- .../Checkout/Test/Block/Cart/Totals.php | 1 + .../AddProductsToShoppingCartEntityTest.php | 92 +++++++++++++------ .../Test/Repository/GroupedProduct.xml | 29 ++++++ .../Repository/GroupedProduct/Associated.xml | 21 +++++ .../GroupedProduct/CheckoutData.xml | 27 ++++++ 5 files changed, 142 insertions(+), 28 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index e29a19a732409..7e1c3ac5ff599 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -105,6 +105,7 @@ class Totals extends Block */ public function getGrandTotal() { + $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); return $this->escapeCurrency($grandTotal); } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php index 365cebdea7626..fce947c12786b 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php @@ -8,10 +8,10 @@ use Magento\Catalog\Test\Page\Product\CatalogProductView; use Magento\Checkout\Test\Page\CheckoutCart; -use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\ObjectManager; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\TestStep\TestStepFactory; /** * Preconditions: @@ -25,6 +25,7 @@ * * @group Shopping_Cart_(CS) * @ZephyrId MAGETWO-25382 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddProductsToShoppingCartEntityTest extends Injectable { @@ -34,83 +35,97 @@ class AddProductsToShoppingCartEntityTest extends Injectable /* end tags */ /** - * Browser interface - * - * @var BrowserInterface - */ - protected $browser; - - /** - * Fixture factory + * Fixture factory. * * @var FixtureFactory */ protected $fixtureFactory; /** - * Catalog product view page + * Catalog product view page. * * @var CatalogProductView */ protected $catalogProductView; /** - * Checkout cart page + * Checkout cart page. * * @var CheckoutCart */ protected $cartPage; /** - * Prepare test data + * Test step creation factory. + * + * @var TestStepFactory + */ + protected $testStepFactory; + + /** + * Configuration data. + * + * @var string + */ + protected $configData; + + /** + * Prepare test data. * - * @param BrowserInterface $browser - * @param FixtureFactory $fixtureFactory * @param CatalogProductView $catalogProductView * @param CheckoutCart $cartPage + * @param TestStepFactory $testStepFactory + * @param FixtureFactory $fixtureFactory * @return void */ public function __prepare( - BrowserInterface $browser, - FixtureFactory $fixtureFactory, CatalogProductView $catalogProductView, - CheckoutCart $cartPage + CheckoutCart $cartPage, + TestStepFactory $testStepFactory, + FixtureFactory $fixtureFactory ) { - $this->browser = $browser; - $this->fixtureFactory = $fixtureFactory; $this->catalogProductView = $catalogProductView; $this->cartPage = $cartPage; + $this->testStepFactory = $testStepFactory; + $this->fixtureFactory = $fixtureFactory; } /** - * Run test add products to shopping cart + * Run test add products to shopping cart. * * @param string $productsData * @param array $cart + * @param string $configData * @return array */ - public function test($productsData, array $cart) + public function test($productsData, array $cart, $configData = null) { + $this->configData = $configData; + // Preconditions $products = $this->prepareProducts($productsData); + $this->setupConfiguration(); // Steps $this->addToCart($products); $cart['data']['items'] = ['products' => $products]; - return ['cart' => $this->fixtureFactory->createByCode('cart', $cart)]; + return [ + 'cart' => $this->fixtureFactory->createByCode('cart', $cart), + 'product' => array_shift($products) + ]; } /** - * Create products + * Create products. * * @param string $productList * @return array */ protected function prepareProducts($productList) { - $addToCartStep = ObjectManager::getInstance()->create( - 'Magento\Catalog\Test\TestStep\CreateProductsStep', + $addToCartStep = $this->testStepFactory->create( + \Magento\Catalog\Test\TestStep\CreateProductsStep::class, ['products' => $productList] ); @@ -119,17 +134,38 @@ protected function prepareProducts($productList) } /** - * Add products to cart + * Add products to cart. * * @param array $products * @return void */ protected function addToCart(array $products) { - $addToCartStep = ObjectManager::getInstance()->create( - 'Magento\Checkout\Test\TestStep\AddProductsToTheCartStep', + $addToCartStep = $this->testStepFactory->create( + \Magento\Checkout\Test\TestStep\AddProductsToTheCartStep::class, ['products' => $products] ); $addToCartStep->run(); } + + /** + * Setup configuration. + * + * @return void + */ + private function setupConfiguration() + { + $this->testStepFactory->create( + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, + ['configData' => $this->configData] + )->run(); + } + + protected function tearDown() + { + $this->testStepFactory->create( + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, + ['configData' => $this->configData] + )->cleanup(); + } } diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml index c4ba219dfe4d3..5b09e5f46c408 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml @@ -117,5 +117,34 @@ grouped_three_simple_products + + + Grouped product %isolation% + grouped_product_%isolation% + + default + + + two_simple_products + + Yes + Catalog, Search + + taxable_goods + + test-grouped-product-%isolation% + + In Stock + + + Main Website + + + default + + + grouped_two_simple_products + + diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/Associated.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/Associated.xml index e4ab3d928b213..a4edcb3626961 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/Associated.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/Associated.xml @@ -146,5 +146,26 @@ catalogProductSimple::product_100_dollar + + + + + %id% + %item1_simple::getProductName% + %position% + 1 + + + %id% + %item1_simple::getProductName% + %position% + 1 + + + + catalogProductSimple::default + catalogProductSimple::product_40_dollar + + diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/CheckoutData.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/CheckoutData.xml index bd60577b99ecd..e7919cbdce791 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/CheckoutData.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct/CheckoutData.xml @@ -40,5 +40,32 @@ + + + + + product_key_0 + 1 + + + product_key_1 + 1 + + + + + 560 + 40 + + + 1 + 1 + + + 560 + 40 + + + From 99e39f9d734598b58126adef751965b68813d6d6 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Thu, 4 May 2017 18:06:13 +0300 Subject: [PATCH 037/363] MAGETWO-57051: [Backport] - [GitHub] UTF-8 special character issue in widgets #4232 - for 2.1 --- .../tests/app/Magento/Cms/Test/Block/Page.php | 14 ++++ .../Test/Constraint/AssertCmsWidgetTitle.php | 66 +++++++++++++++++++ .../Cms/Test/Repository/CmsPage/Content.xml | 12 ++++ .../Test/TestCase/CreateCmsPageEntityTest.xml | 12 ++++ .../InjectableTests/MAGETWO-57051.xml | 15 +++++ lib/web/legacy-build.min.js | 2 +- lib/web/mage/adminhtml/tools.js | 10 +-- 7 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Cms/Test/Constraint/AssertCmsWidgetTitle.php create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Page.php b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Page.php index a998c5003ed6f..6e583638f1a46 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Page.php +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Page.php @@ -122,4 +122,18 @@ public function waitPageInit() $this->waitForElementNotVisible($this->initialScript); sleep(3); // TODO: remove after resolving an issue with ajax on Frontend. } + + /** + * Get widget title value. + * + * @param string $widgetType + * @param string $widgetText + * @return string + */ + public function getWidgetTitle($widgetType, $widgetText) + { + return $this->_rootElement + ->find(sprintf($this->widgetSelectors[$widgetType], $widgetText), Locator::SELECTOR_XPATH) + ->getAttribute('title'); + } } diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Constraint/AssertCmsWidgetTitle.php b/dev/tests/functional/tests/app/Magento/Cms/Test/Constraint/AssertCmsWidgetTitle.php new file mode 100644 index 0000000000000..4a0027324bb09 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Constraint/AssertCmsWidgetTitle.php @@ -0,0 +1,66 @@ +open(); + $filter = ['title' => $cms->getTitle()]; + $cmsIndex->getCmsPageGridBlock()->searchAndPreview($filter); + $browser->selectWindow(); + + $fixtureContent = $cms->getContent(); + + if (isset($fixtureContent['widget'])) { + foreach ($fixtureContent['widget']['dataset'] as $widget) { + \PHPUnit_Framework_Assert::assertEquals( + $widget['title'], + $frontCmsPage->getCmsPageBlock()->getWidgetTitle($widget['widget_type'], $widget['anchor_text']), + "Widget title wasn't properly saved." + ); + } + } + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Widget title equals to data from fixture.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Repository/CmsPage/Content.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/Repository/CmsPage/Content.xml index f3de18fa21e99..1d9cdd68b3bab 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/Repository/CmsPage/Content.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Repository/CmsPage/Content.xml @@ -74,5 +74,17 @@ Viewed Products Grid Template + + + + CMS Page Link + CMS Page Link anchor_text_%isolation% + glāžšķūņu rūķīši_%isolation% + CMS Page Link Block Template + + home + + + diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityTest.xml index 96860dc6c80eb..411aacdc120b1 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityTest.xml @@ -58,5 +58,17 @@ + + to_maintain:yes + cmsPage + Yes + NewCmsPage%isolation% + identifier-%isolation% + Main Website/Main Website Store/Default Store View + cms_page_text_content%isolation% + widget_with_special_characters_in_title + + + diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml new file mode 100644 index 0000000000000..18db002001ef6 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/lib/web/legacy-build.min.js b/lib/web/legacy-build.min.js index 289cf49e76a13..dd618319b1884 100644 --- a/lib/web/legacy-build.min.js +++ b/lib/web/legacy-build.min.js @@ -5,4 +5,4 @@ var Prototype={Version:"1.7",Browser:(function(){var d=navigator.userAgent;var b * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ -(function(){var w=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,p=0,g=Object.prototype.toString,u=false,o=true;[0,0].sort(function(){o=false;return 0});var d=function(L,B,I,D){I=I||[];var e=B=B||document;if(B.nodeType!==1&&B.nodeType!==9){return[]}if(!L||typeof L!=="string"){return I}var J=[],K,G,P,O,H,A,z=true,E=v(B),N=L;while((w.exec(""),K=w.exec(N))!==null){N=K[3];J.push(K[1]);if(K[2]){A=K[3];break}}if(J.length>1&&q.exec(L)){if(J.length===2&&h.relative[J[0]]){G=l(J[0]+J[1],B)}else{G=h.relative[J[0]]?[B]:d(J.shift(),B);while(J.length){L=J.shift();if(h.relative[L]){L+=J.shift()}G=l(L,G)}}}else{if(!D&&J.length>1&&B.nodeType===9&&!E&&h.match.ID.test(J[0])&&!h.match.ID.test(J[J.length-1])){var Q=d.find(J.shift(),B,E);B=Q.expr?d.filter(Q.expr,Q.set)[0]:Q.set[0]}if(B){var Q=D?{expr:J.pop(),set:b(D)}:d.find(J.pop(),J.length===1&&(J[0]==="~"||J[0]==="+")&&B.parentNode?B.parentNode:B,E);G=Q.expr?d.filter(Q.expr,Q.set):Q.set;if(J.length>0){P=b(G)}else{z=false}while(J.length){var C=J.pop(),F=C;if(!h.relative[C]){C=""}else{F=J.pop()}if(F==null){F=B}h.relative[C](P,F,E)}}else{P=J=[]}}if(!P){P=G}if(!P){throw"Syntax error, unrecognized expression: "+(C||L)}if(g.call(P)==="[object Array]"){if(!z){I.push.apply(I,P)}else{if(B&&B.nodeType===1){for(var M=0;P[M]!=null;M++){if(P[M]&&(P[M]===true||P[M].nodeType===1&&n(B,P[M]))){I.push(G[M])}}}else{for(var M=0;P[M]!=null;M++){if(P[M]&&P[M].nodeType===1){I.push(G[M])}}}}}else{b(P,I)}if(A){d(A,e,I,D);d.uniqueSort(I)}return I};d.uniqueSort=function(z){if(f){u=o;z.sort(f);if(u){for(var e=1;e":function(E,z,F){var C=typeof z==="string";if(C&&!/\W/.test(z)){z=F?z:z.toUpperCase();for(var A=0,e=E.length;A=0)){if(!A){e.push(D)}}else{if(A){z[C]=false}}}}return false},ID:function(e){return e[1].replace(/\\/g,"")},TAG:function(z,e){for(var A=0;e[A]===false;A++){}return e[A]&&v(e[A])?z[1]:z[1].toUpperCase()},CHILD:function(e){if(e[1]=="nth"){var z=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(e[2]=="even"&&"2n"||e[2]=="odd"&&"2n+1"||!/\D/.test(e[2])&&"0n+"+e[2]||e[2]);e[2]=(z[1]+(z[2]||1))-0;e[3]=z[3]-0}e[0]=p++;return e},ATTR:function(C,z,A,e,D,E){var B=C[1].replace(/\\/g,"");if(!E&&h.attrMap[B]){C[1]=h.attrMap[B]}if(C[2]==="~="){C[4]=" "+C[4]+" "}return C},PSEUDO:function(C,z,A,e,D){if(C[1]==="not"){if((w.exec(C[3])||"").length>1||/^\w/.test(C[3])){C[3]=d(C[3],null,null,z)}else{var B=d.filter(C[3],z,A,true^D);if(!A){e.push.apply(e,B)}return false}}else{if(h.match.POS.test(C[0])||h.match.CHILD.test(C[0])){return true}}return C},POS:function(e){e.unshift(true);return e}},filters:{enabled:function(e){return e.disabled===false&&e.type!=="hidden"},disabled:function(e){return e.disabled===true},checked:function(e){return e.checked===true},selected:function(e){e.parentNode.selectedIndex;return e.selected===true},parent:function(e){return !!e.firstChild},empty:function(e){return !e.firstChild},has:function(A,z,e){return !!d(e[3],A).length},header:function(e){return/h\d/i.test(e.nodeName)},text:function(e){return"text"===e.type},radio:function(e){return"radio"===e.type},checkbox:function(e){return"checkbox"===e.type},file:function(e){return"file"===e.type},password:function(e){return"password"===e.type},submit:function(e){return"submit"===e.type},image:function(e){return"image"===e.type},reset:function(e){return"reset"===e.type},button:function(e){return"button"===e.type||e.nodeName.toUpperCase()==="BUTTON"},input:function(e){return/input|select|textarea|button/i.test(e.nodeName)}},setFilters:{first:function(z,e){return e===0},last:function(A,z,e,B){return z===B.length-1},even:function(z,e){return e%2===0},odd:function(z,e){return e%2===1},lt:function(A,z,e){return ze[3]-0},nth:function(A,z,e){return e[3]-0==z},eq:function(A,z,e){return e[3]-0==z}},filter:{PSEUDO:function(E,A,B,F){var z=A[1],C=h.filters[z];if(C){return C(E,B,A,F)}else{if(z==="contains"){return(E.textContent||E.innerText||"").indexOf(A[3])>=0}else{if(z==="not"){var D=A[3];for(var B=0,e=D.length;B=0)}}},ID:function(z,e){return z.nodeType===1&&z.getAttribute("id")===e},TAG:function(z,e){return(e==="*"&&z.nodeType===1)||z.nodeName===e},CLASS:function(z,e){return(" "+(z.className||z.getAttribute("class"))+" ").indexOf(e)>-1},ATTR:function(D,B){var A=B[1],e=h.attrHandle[A]?h.attrHandle[A](D):D[A]!=null?D[A]:D.getAttribute(A),E=e+"",C=B[2],z=B[4];return e==null?C==="!=":C==="="?E===z:C==="*="?E.indexOf(z)>=0:C==="~="?(" "+E+" ").indexOf(z)>=0:!z?E&&e!==false:C==="!="?E!=z:C==="^="?E.indexOf(z)===0:C==="$="?E.substr(E.length-z.length)===z:C==="|="?E===z||E.substr(0,z.length+1)===z+"-":false},POS:function(C,z,A,D){var e=z[2],B=h.setFilters[e];if(B){return B(C,A,z,D)}}}};var q=h.match.POS;for(var s in h.match){h.match[s]=new RegExp(h.match[s].source+/(?![^\[]*\])(?![^\(]*\))/.source);h.leftMatch[s]=new RegExp(/(^(?:.|\r|\n)*?)/.source+h.match[s].source)}var b=function(z,e){z=Array.prototype.slice.call(z,0);if(e){e.push.apply(e,z);return e}return z};try{Array.prototype.slice.call(document.documentElement.childNodes,0)}catch(r){b=function(C,B){var z=B||[];if(g.call(C)==="[object Array]"){Array.prototype.push.apply(z,C)}else{if(typeof C.length==="number"){for(var A=0,e=C.length;A";var e=document.documentElement;e.insertBefore(z,e.firstChild);if(!!document.getElementById(A)){h.find.ID=function(C,D,E){if(typeof D.getElementById!=="undefined"&&!E){var B=D.getElementById(C[1]);return B?B.id===C[1]||typeof B.getAttributeNode!=="undefined"&&B.getAttributeNode("id").nodeValue===C[1]?[B]:undefined:[]}};h.filter.ID=function(D,B){var C=typeof D.getAttributeNode!=="undefined"&&D.getAttributeNode("id");return D.nodeType===1&&C&&C.nodeValue===B}}e.removeChild(z);e=z=null})();(function(){var e=document.createElement("div");e.appendChild(document.createComment(""));if(e.getElementsByTagName("*").length>0){h.find.TAG=function(z,D){var C=D.getElementsByTagName(z[1]);if(z[1]==="*"){var B=[];for(var A=0;C[A];A++){if(C[A].nodeType===1){B.push(C[A])}}C=B}return C}}e.innerHTML="";if(e.firstChild&&typeof e.firstChild.getAttribute!=="undefined"&&e.firstChild.getAttribute("href")!=="#"){h.attrHandle.href=function(z){return z.getAttribute("href",2)}}e=null})();if(document.querySelectorAll){(function(){var e=d,A=document.createElement("div");A.innerHTML="

";if(A.querySelectorAll&&A.querySelectorAll(".TEST").length===0){return}d=function(E,D,B,C){D=D||document;if(!C&&D.nodeType===9&&!v(D)){try{return b(D.querySelectorAll(E),B)}catch(F){}}return e(E,D,B,C)};for(var z in e){d[z]=e[z]}A=null})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var e=document.createElement("div");e.innerHTML="
";if(e.getElementsByClassName("e").length===0){return}e.lastChild.className="e";if(e.getElementsByClassName("e").length===1){return}h.order.splice(1,0,"CLASS");h.find.CLASS=function(z,A,B){if(typeof A.getElementsByClassName!=="undefined"&&!B){return A.getElementsByClassName(z[1])}};e=null})()}function t(z,E,D,I,F,H){var G=z=="previousSibling"&&!H;for(var B=0,A=I.length;B0){C=e;break}}}e=e[z]}I[B]=C}}}var n=document.compareDocumentPosition?function(z,e){return z.compareDocumentPosition(e)&16}:function(z,e){return z!==e&&(z.contains?z.contains(e):true)};var v=function(e){return e.nodeType===9&&e.documentElement.nodeName!=="HTML"||!!e.ownerDocument&&e.ownerDocument.documentElement.nodeName!=="HTML"};var l=function(e,F){var B=[],C="",D,A=F.nodeType?[F]:F;while((D=h.match.PSEUDO.exec(e))){C+=D[0];e=e.replace(h.match.PSEUDO,"")}e=h.relative[e]?e+"*":e;for(var E=0,z=A.length;E=0}).sortBy(function(f){return f.tabIndex}).first();return b?b:e.find(function(f){return/^(?:input|select|textarea)$/i.test(f.tagName)})},focusFirstElement:function(d){d=$(d);var b=d.findFirstElement();if(b){b.activate()}return d},request:function(d,b){d=$(d),b=Object.clone(b||{});var f=b.parameters,e=d.readAttribute("action")||"";if(e.blank()){e=window.location.href}b.parameters=d.serialize(true);if(f){if(Object.isString(f)){f=f.toQueryParams()}Object.extend(b.parameters,f)}if(d.hasAttribute("method")&&!b.method){b.method=d.method}return new Ajax.Request(e,b)}};Form.Element={focus:function(b){$(b).focus();return b},select:function(b){$(b).select();return b}};Form.Element.Methods={serialize:function(b){b=$(b);if(!b.disabled&&b.name){var d=b.getValue();if(d!=undefined){var e={};e[b.name]=d;return Object.toQueryString(e)}}return""},getValue:function(b){b=$(b);var d=b.tagName.toLowerCase();return Form.Element.Serializers[d](b)},setValue:function(b,d){b=$(b);var e=b.tagName.toLowerCase();Form.Element.Serializers[e](b,d);return b},clear:function(b){$(b).value="";return b},present:function(b){return $(b).value!=""},activate:function(b){b=$(b);try{b.focus();if(b.select&&(b.tagName.toLowerCase()!="input"||!(/^(?:button|reset|submit)$/i.test(b.type)))){b.select()}}catch(d){}return b},disable:function(b){b=$(b);b.disabled=true;return b},enable:function(b){b=$(b);b.disabled=false;return b}};var Field=Form.Element;var $F=Form.Element.Methods.getValue;Form.Element.Serializers=(function(){function d(n,o){switch(n.type.toLowerCase()){case"checkbox":case"radio":return h(n,o);default:return g(n,o)}}function h(n,o){if(Object.isUndefined(o)){return n.checked?n.value:null}else{n.checked=!!o}}function g(n,o){if(Object.isUndefined(o)){return n.value}else{n.value=o}}function b(p,s){if(Object.isUndefined(s)){return(p.type==="select-one"?e:f)(p)}var o,q,t=!Object.isArray(s);for(var n=0,r=p.length;n=0?l(o.options[n]):null}function f(q){var n,r=q.length;if(!r){return null}for(var p=0,n=[];p=this.offset[1]&&e=this.offset[0]&&b=this.offset[1]&&this.ycomp=this.offset[0]&&this.xcomp0})._each(b)},set:function(b){this.element.className=b},add:function(b){if(this.include(b)){return}this.set($A(this).concat(b).join(" "))},remove:function(b){if(!this.include(b)){return}this.set($A(this).without(b).join(" "))},toString:function(){return $A(this).join(" ")}};Object.extend(Element.ClassNames.prototype,Enumerable);(function(){window.Selector=Class.create({initialize:function(b){this.expression=b.strip()},findElements:function(b){return Prototype.Selector.select(this.expression,b)},match:function(b){return Prototype.Selector.match(b,this.expression)},toString:function(){return this.expression},inspect:function(){return"#"}});Object.extend(Selector,{matchElements:function(h,l){var b=Prototype.Selector.match,f=[];for(var e=0,g=h.length;e0){if(typeof arguments[0]=="string"){e=arguments[0];d=1}else{e=arguments[0]?arguments[0].id:null}}if(!e){e="window_"+new Date().getTime()}if($(e)){alert("Window "+e+" is already registered in the DOM! Make sure you use setDestroyOnClose() or destroyOnClose: true in the constructor")}this.options=Object.extend({className:"dialog",windowClassName:null,blurClassName:null,minWidth:100,minHeight:20,resizable:true,closable:true,minimizable:true,maximizable:true,draggable:true,userData:null,showEffect:(Window.hasEffectLib?Effect.Appear:Element.show),hideEffect:(Window.hasEffectLib?Effect.Fade:Element.hide),showEffectOptions:{},hideEffectOptions:{},effectOptions:null,parent:document.body,title:" ",url:null,onload:Prototype.emptyFunction,width:200,height:300,opacity:1,recenterAuto:true,wiredDrag:false,closeOnEsc:true,closeCallback:null,destroyOnClose:false,gridX:1,gridY:1},arguments[d]||{});if(this.options.blurClassName){this.options.focusClassName=this.options.className}if(typeof this.options.top=="undefined"&&typeof this.options.bottom=="undefined"){this.options.top=this._round(Math.random()*500,this.options.gridY)}if(typeof this.options.left=="undefined"&&typeof this.options.right=="undefined"){this.options.left=this._round(Math.random()*500,this.options.gridX)}if(this.options.effectOptions){Object.extend(this.options.hideEffectOptions,this.options.effectOptions);Object.extend(this.options.showEffectOptions,this.options.effectOptions);if(this.options.showEffect==Element.Appear){this.options.showEffectOptions.to=this.options.opacity}}if(Window.hasEffectLib){if(this.options.showEffect==Effect.Appear){this.options.showEffectOptions.to=this.options.opacity}if(this.options.hideEffect==Effect.Fade){this.options.hideEffectOptions.from=this.options.opacity}}if(this.options.hideEffect==Element.hide){this.options.hideEffect=function(){Element.hide(this.element);if(this.options.destroyOnClose){this.destroy()}}.bind(this)}if(this.options.parent!=document.body){this.options.parent=$(this.options.parent)}this.element=this._createWindow(e);this.element.win=this;this.eventMouseDown=this._initDrag.bindAsEventListener(this);this.eventMouseUp=this._endDrag.bindAsEventListener(this);this.eventMouseMove=this._updateDrag.bindAsEventListener(this);this.eventOnLoad=this._getWindowBorderSize.bindAsEventListener(this);this.eventMouseDownContent=this.toFront.bindAsEventListener(this);this.eventResize=this._recenter.bindAsEventListener(this);this.eventKeyUp=this._keyUp.bindAsEventListener(this);this.topbar=$(this.element.id+"_top");this.bottombar=$(this.element.id+"_bottom");this.content=$(this.element.id+"_content");Event.observe(this.topbar,"mousedown",this.eventMouseDown);Event.observe(this.bottombar,"mousedown",this.eventMouseDown);Event.observe(this.content,"mousedown",this.eventMouseDownContent);Event.observe(window,"load",this.eventOnLoad);Event.observe(window,"resize",this.eventResize);Event.observe(window,"scroll",this.eventResize);Event.observe(document,"keyup",this.eventKeyUp);Event.observe(this.options.parent,"scroll",this.eventResize);if(this.options.draggable){var b=this;[this.topbar,this.topbar.up().previous(),this.topbar.up().next()].each(function(f){f.observe("mousedown",b.eventMouseDown);f.addClassName("top_draggable")});[this.bottombar.up(),this.bottombar.up().previous(),this.bottombar.up().next()].each(function(f){f.observe("mousedown",b.eventMouseDown);f.addClassName("bottom_draggable")})}if(this.options.resizable){this.sizer=$(this.element.id+"_sizer");Event.observe(this.sizer,"mousedown",this.eventMouseDown)}this.useLeft=null;this.useTop=null;if(typeof this.options.left!="undefined"){this.element.setStyle({left:parseFloat(this.options.left)+"px"});this.useLeft=true}else{this.element.setStyle({right:parseFloat(this.options.right)+"px"});this.useLeft=false}if(typeof this.options.top!="undefined"){this.element.setStyle({top:parseFloat(this.options.top)+"px"});this.useTop=true}else{this.element.setStyle({bottom:parseFloat(this.options.bottom)+"px"});this.useTop=false}this.storedLocation=null;this.setOpacity(this.options.opacity);if(this.options.zIndex){this.setZIndex(this.options.zIndex)}else{this.setZIndex(this.getMaxZIndex())}if(this.options.destroyOnClose){this.setDestroyOnClose(true)}this._getWindowBorderSize();this.width=this.options.width;this.height=this.options.height;this.visible=false;this.constraint=false;this.constraintPad={top:0,left:0,bottom:0,right:0};if(this.width&&this.height){this.setSize(this.options.width,this.options.height)}this.setTitle(this.options.title);Windows.register(this)},getMaxZIndex:function(){var b=0,d;var g=document.body.childNodes;for(d=0;d ';$(this.getId()+"_table_content").innerHTML=d;this.content=$(this.element.id+"_content")}this.getContent().innerHTML=b},setAjaxContent:function(d,b,f,e){this.showFunction=f?"showCenter":"show";this.showModal=e||false;b=b||{};this.setHTMLContent("");this.onComplete=b.onComplete;if(!this._onCompleteHandler){this._onCompleteHandler=this._setAjaxContent.bind(this)}b.onComplete=this._onCompleteHandler;new Ajax.Request(d,b);b.onComplete=this.onComplete},_setAjaxContent:function(b){Element.update(this.getContent(),b.responseText);if(this.onComplete){this.onComplete(b)}this.onComplete=null;this[this.showFunction](this.showModal)},setURL:function(b){if(this.options.url){this.content.src=null}this.options.url=b;var d="";$(this.getId()+"_table_content").innerHTML=d;this.content=$(this.element.id+"_content")},getURL:function(){return this.options.url?this.options.url:null},refresh:function(){if(this.options.url){$(this.element.getAttribute("id")+"_content").src=this.options.url}},setCookie:function(d,e,t,g,b){d=d||this.element.id;this.cookie=[d,e,t,g,b];var r=WindowUtilities.getCookie(d);if(r){var s=r.split(",");var p=s[0].split(":");var o=s[1].split(":");var q=parseFloat(s[2]),l=parseFloat(s[3]);var n=s[4];var f=s[5];this.setSize(q,l);if(n=="true"){this.doMinimize=true}else{if(f=="true"){this.doMaximize=true}}this.useLeft=p[0]=="l";this.useTop=o[0]=="t";this.element.setStyle(this.useLeft?{left:p[1]}:{right:p[1]});this.element.setStyle(this.useTop?{top:o[1]}:{bottom:o[1]})}},getId:function(){return this.element.id},setDestroyOnClose:function(){this.options.destroyOnClose=true},setConstraint:function(b,d){this.constraint=b;this.constraintPad=Object.extend(this.constraintPad,d||{});if(this.useTop&&this.useLeft){this.setLocation(parseFloat(this.element.style.top),parseFloat(this.element.style.left))}},_initDrag:function(d){if(Event.element(d)==this.sizer&&this.isMinimized()){return}if(Event.element(d)!=this.sizer&&this.isMaximized()){return}if(Prototype.Browser.IE&&this.heightN==0){this._getWindowBorderSize()}this.pointer=[this._round(Event.pointerX(d),this.options.gridX),this._round(Event.pointerY(d),this.options.gridY)];if(this.options.wiredDrag){this.currentDrag=this._createWiredElement()}else{this.currentDrag=this.element}if(Event.element(d)==this.sizer){this.doResize=true;this.widthOrg=this.width;this.heightOrg=this.height;this.bottomOrg=parseFloat(this.element.getStyle("bottom"));this.rightOrg=parseFloat(this.element.getStyle("right"));this._notify("onStartResize")}else{this.doResize=false;var b=$(this.getId()+"_close");if(b&&Position.within(b,this.pointer[0],this.pointer[1])){this.currentDrag=null;return}this.toFront();if(!this.options.draggable){return}this._notify("onStartMove")}Event.observe(document,"mouseup",this.eventMouseUp,false);Event.observe(document,"mousemove",this.eventMouseMove,false);WindowUtilities.disableScreen("__invisible__","__invisible__",this.overlayOpacity);document.body.ondrag=function(){return false};document.body.onselectstart=function(){return false};this.currentDrag.show();Event.stop(d)},_round:function(d,b){return b==1?d:d=Math.floor(d/b)*b},_updateDrag:function(d){var b=[this._round(Event.pointerX(d),this.options.gridX),this._round(Event.pointerY(d),this.options.gridY)];var q=b[0]-this.pointer[0];var p=b[1]-this.pointer[1];if(this.doResize){var o=this.widthOrg+q;var f=this.heightOrg+p;q=this.width-this.widthOrg;p=this.height-this.heightOrg;if(this.useLeft){o=this._updateWidthConstraint(o)}else{this.currentDrag.setStyle({right:(this.rightOrg-q)+"px"})}if(this.useTop){f=this._updateHeightConstraint(f)}else{this.currentDrag.setStyle({bottom:(this.bottomOrg-p)+"px"})}this.setSize(o,f);this._notify("onResize")}else{this.pointer=b;if(this.useLeft){var e=parseFloat(this.currentDrag.getStyle("left"))+q;var n=this._updateLeftConstraint(e);this.pointer[0]+=n-e;this.currentDrag.setStyle({left:n+"px"})}else{this.currentDrag.setStyle({right:parseFloat(this.currentDrag.getStyle("right"))-q+"px"})}if(this.useTop){var l=parseFloat(this.currentDrag.getStyle("top"))+p;var g=this._updateTopConstraint(l);this.pointer[1]+=g-l;this.currentDrag.setStyle({top:g+"px"})}else{this.currentDrag.setStyle({bottom:parseFloat(this.currentDrag.getStyle("bottom"))-p+"px"})}this._notify("onMove")}if(this.iefix){this._fixIEOverlapping()}this._removeStoreLocation();Event.stop(d)},_endDrag:function(b){WindowUtilities.enableScreen("__invisible__");if(this.doResize){this._notify("onEndResize")}else{this._notify("onEndMove")}Event.stopObserving(document,"mouseup",this.eventMouseUp,false);Event.stopObserving(document,"mousemove",this.eventMouseMove,false);Event.stop(b);this._hideWiredElement();this._saveCookie();document.body.ondrag=null;document.body.onselectstart=null},_updateLeftConstraint:function(d){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowWidth:this.options.parent.getDimensions().width;if(db-this.constraintPad.right){d=b-this.constraintPad.right-this.width-this.widthE-this.widthW}}return d},_updateTopConstraint:function(e){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowHeight:this.options.parent.getDimensions().height;var d=this.height+this.heightN+this.heightS;if(eb-this.constraintPad.bottom){e=b-this.constraintPad.bottom-d}}return e},_updateWidthConstraint:function(b){if(this.constraint&&this.useLeft&&this.useTop){var d=this.options.parent==document.body?WindowUtilities.getPageSize().windowWidth:this.options.parent.getDimensions().width;var e=parseFloat(this.element.getStyle("left"));if(e+b+this.widthE+this.widthW>d-this.constraintPad.right){b=d-this.constraintPad.right-e-this.widthE-this.widthW}}return b},_updateHeightConstraint:function(d){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowHeight:this.options.parent.getDimensions().height;var e=parseFloat(this.element.getStyle("top"));if(e+d+this.heightN+this.heightS>b-this.constraintPad.bottom){d=b-this.constraintPad.bottom-e-this.heightN-this.heightS}}return d},_createWindow:function(b){var h=this.options.className;var f=document.createElement("div");f.setAttribute("id",b);f.className="dialog";if(this.options.windowClassName){f.className+=" "+this.options.windowClassName}var g;if(this.options.url){g=''}else{g='
'}var l=this.options.closable?"
":"";var n=this.options.minimizable?"
":"";var o=this.options.maximizable?"
":"";var e=this.options.resizable?"class='"+h+"_sizer' id='"+b+"_sizer'":"class='"+h+"_se'";var d="../themes/default/blank.gif";f.innerHTML=l+n+o+"
"+this.options.title+"
"+g+"
";Element.hide(f);this.options.parent.insertBefore(f,this.options.parent.firstChild);Event.observe($(b+"_content"),"load",this.options.onload);return f},changeClassName:function(b){var d=this.options.className;var e=this.getId();$A(["_close","_minimize","_maximize","_sizer","_content"]).each(function(f){this._toggleClassName($(e+f),d+f,b+f)}.bind(this));this._toggleClassName($(e+"_top"),d+"_title",b+"_title");$$("#"+e+" td").each(function(f){f.className=f.className.sub(d,b)});this.options.className=b},_toggleClassName:function(e,d,b){if(e){e.removeClassName(d);e.addClassName(b)}},setLocation:function(f,d){f=this._updateTopConstraint(f);d=this._updateLeftConstraint(d);var b=this.currentDrag||this.element;b.setStyle({top:f+"px"});b.setStyle({left:d+"px"});this.useLeft=true;this.useTop=true},getLocation:function(){var b={};if(this.useTop){b=Object.extend(b,{top:this.element.getStyle("top")})}else{b=Object.extend(b,{bottom:this.element.getStyle("bottom")})}if(this.useLeft){b=Object.extend(b,{left:this.element.getStyle("left")})}else{b=Object.extend(b,{right:this.element.getStyle("right")})}return b},getSize:function(){return{width:this.width,height:this.height}},setSize:function(f,d,b){f=parseFloat(f);d=parseFloat(d);if(!this.minimized&&fthis.options.maxHeight){d=this.options.maxHeight}if(this.options.maxWidth&&f>this.options.maxWidth){f=this.options.maxWidth}if(this.useTop&&this.useLeft&&Window.hasEffectLib&&Effect.ResizeWindow&&b){new Effect.ResizeWindow(this,null,null,f,d,{duration:Window.resizeEffectDuration})}else{this.width=f;this.height=d;var h=this.currentDrag?this.currentDrag:this.element;h.setStyle({width:f+this.widthW+this.widthE+"px"});h.setStyle({height:d+this.heightN+this.heightS+"px"});if(!this.currentDrag||this.currentDrag==this.element){var g=$(this.element.id+"_content");g.setStyle({height:d+"px"});g.setStyle({width:f+"px"})}}},updateHeight:function(){this.setSize(this.width,this.content.scrollHeight,true)},updateWidth:function(){this.setSize(this.content.scrollWidth,this.height,true)},toFront:function(){if(this.element.style.zIndex0)&&(navigator.userAgent.indexOf("Opera")<0)&&(this.element.getStyle("position")=="absolute")){new Insertion.After(this.element.id,'');this.iefix=$(this.element.id+"_iefix")}if(this.iefix){setTimeout(this._fixIEOverlapping.bind(this),50)}},_fixIEOverlapping:function(){Position.clone(this.element,this.iefix);this.iefix.style.zIndex=this.element.style.zIndex-1;this.iefix.show()},_keyUp:function(b){if(27==b.keyCode&&this.options.closeOnEsc){this.close()}},_getWindowBorderSize:function(d){var e=this._createHiddenDiv(this.options.className+"_n");this.heightN=Element.getDimensions(e).height;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_s");this.heightS=Element.getDimensions(e).height;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_e");this.widthE=Element.getDimensions(e).width;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_w");this.widthW=Element.getDimensions(e).width;e.parentNode.removeChild(e);var e=document.createElement("div");e.className="overlay_"+this.options.className;document.body.appendChild(e);var b=this;setTimeout(function(){b.overlayOpacity=($(e).getStyle("opacity"));e.parentNode.removeChild(e)},10);if(Prototype.Browser.IE){this.heightS=$(this.getId()+"_row3").getDimensions().height;this.heightN=$(this.getId()+"_row1").getDimensions().height}if(Prototype.Browser.WebKit&&Prototype.Browser.WebKitVersion<420){this.setSize(this.width,this.height)}if(this.doMaximize){this.maximize()}if(this.doMinimize){this.minimize()}},_createHiddenDiv:function(d){var b=document.body;var e=document.createElement("div");e.setAttribute("id",this.element.id+"_tmp");e.className=d;e.style.display="none";e.innerHTML="";b.insertBefore(e,b.firstChild);return e},_storeLocation:function(){if(this.storedLocation==null){this.storedLocation={useTop:this.useTop,useLeft:this.useLeft,top:this.element.getStyle("top"),bottom:this.element.getStyle("bottom"),left:this.element.getStyle("left"),right:this.element.getStyle("right"),width:this.width,height:this.height}}},_restoreLocation:function(){if(this.storedLocation!=null){this.useLeft=this.storedLocation.useLeft;this.useTop=this.storedLocation.useTop;if(this.useLeft&&this.useTop&&Window.hasEffectLib&&Effect.ResizeWindow){new Effect.ResizeWindow(this,this.storedLocation.top,this.storedLocation.left,this.storedLocation.width,this.storedLocation.height,{duration:Window.resizeEffectDuration})}else{this.element.setStyle(this.useLeft?{left:this.storedLocation.left}:{right:this.storedLocation.right});this.element.setStyle(this.useTop?{top:this.storedLocation.top}:{bottom:this.storedLocation.bottom});this.setSize(this.storedLocation.width,this.storedLocation.height)}Windows.resetOverflow();this._removeStoreLocation()}},_removeStoreLocation:function(){this.storedLocation=null},_saveCookie:function(){if(this.cookie){var b="";if(this.useLeft){b+="l:"+(this.storedLocation?this.storedLocation.left:this.element.getStyle("left"))}else{b+="r:"+(this.storedLocation?this.storedLocation.right:this.element.getStyle("right"))}if(this.useTop){b+=",t:"+(this.storedLocation?this.storedLocation.top:this.element.getStyle("top"))}else{b+=",b:"+(this.storedLocation?this.storedLocation.bottom:this.element.getStyle("bottom"))}b+=","+(this.storedLocation?this.storedLocation.width:this.width);b+=","+(this.storedLocation?this.storedLocation.height:this.height);b+=","+this.isMinimized();b+=","+this.isMaximized();WindowUtilities.setCookie(b,this.cookie)}},_createWiredElement:function(){if(!this.wiredElement){if(Prototype.Browser.IE){this._getWindowBorderSize()}var d=document.createElement("div");d.className="wired_frame "+this.options.className+"_wired_frame";d.style.position="absolute";this.options.parent.insertBefore(d,this.options.parent.firstChild);this.wiredElement=$(d)}if(this.useLeft){this.wiredElement.setStyle({left:this.element.getStyle("left")})}else{this.wiredElement.setStyle({right:this.element.getStyle("right")})}if(this.useTop){this.wiredElement.setStyle({top:this.element.getStyle("top")})}else{this.wiredElement.setStyle({bottom:this.element.getStyle("bottom")})}var b=this.element.getDimensions();this.wiredElement.setStyle({width:b.width+"px",height:b.height+"px"});this.wiredElement.setStyle({zIndex:Windows.maxZIndex+30});return this.wiredElement},_hideWiredElement:function(){if(!this.wiredElement||!this.currentDrag){return}if(this.currentDrag==this.element){this.currentDrag=null}else{if(this.useLeft){this.element.setStyle({left:this.currentDrag.getStyle("left")})}else{this.element.setStyle({right:this.currentDrag.getStyle("right")})}if(this.useTop){this.element.setStyle({top:this.currentDrag.getStyle("top")})}else{this.element.setStyle({bottom:this.currentDrag.getStyle("bottom")})}this.currentDrag.hide();this.currentDrag=null;if(this.doResize){this.setSize(this.width,this.height)}}},_notify:function(b){if(this.options[b]){this.options[b](this)}else{Windows.notify(b,this)}}};var Windows={windows:[],modalWindows:[],observers:[],focusedWindow:null,maxZIndex:0,overlayShowEffectOptions:{duration:0.5},overlayHideEffectOptions:{duration:0.5},addObserver:function(b){this.removeObserver(b);this.observers.push(b)},removeObserver:function(b){this.observers=this.observers.reject(function(d){return d==b})},notify:function(b,d){this.observers.each(function(e){if(e[b]){e[b](b,d)}})},getWindow:function(b){return this.windows.detect(function(e){return e.getId()==b})},getFocusedWindow:function(){return this.focusedWindow},updateFocusedWindow:function(){this.focusedWindow=this.windows.length>=2?this.windows[this.windows.length-2]:null},register:function(b){this.windows.push(b)},addModalWindow:function(b){if(this.modalWindows.length==0){WindowUtilities.disableScreen(b.options.className,"overlay_modal",b.overlayOpacity,b.getId(),b.options.parent)}else{if(Window.keepMultiModalWindow){$("overlay_modal").style.zIndex=Windows.maxZIndex+1;Windows.maxZIndex+=1;WindowUtilities._hideSelect(this.modalWindows.last().getId())}else{this.modalWindows.last().element.hide()}WindowUtilities._showSelect(b.getId())}this.modalWindows.push(b)},removeModalWindow:function(b){this.modalWindows.pop();if(this.modalWindows.length==0){WindowUtilities.enableScreen()}else{if(Window.keepMultiModalWindow){this.modalWindows.last().toFront();WindowUtilities._showSelect(this.modalWindows.last().getId())}else{this.modalWindows.last().element.show()}}},register:function(b){this.windows.push(b)},unregister:function(b){this.windows=this.windows.reject(function(e){return e==b})},closeAll:function(){this.windows.each(function(b){Windows.close(b.getId())})},closeAllModalWindows:function(){WindowUtilities.enableScreen();this.modalWindows.each(function(b){if(b){b.close()}})},minimize:function(e,b){var d=this.getWindow(e);if(d&&d.visible){d.minimize()}Event.stop(b)},maximize:function(e,b){var d=this.getWindow(e);if(d&&d.visible){d.maximize()}Event.stop(b)},close:function(e,b){var d=this.getWindow(e);if(d){d.close()}if(b){Event.stop(b)}},blur:function(d){var b=this.getWindow(d);if(!b){return}if(b.options.blurClassName){b.changeClassName(b.options.blurClassName)}if(this.focusedWindow==b){this.focusedWindow=null}b._notify("onBlur")},focus:function(d){var b=this.getWindow(d);if(!b){return}if(this.focusedWindow){this.blur(this.focusedWindow.getId())}if(b.options.focusClassName){b.changeClassName(b.options.focusClassName)}this.focusedWindow=b;b._notify("onFocus")},unsetOverflow:function(b){this.windows.each(function(e){e.oldOverflow=e.getContent().getStyle("overflow")||"auto";e.getContent().setStyle({overflow:"hidden"})});if(b&&b.oldOverflow){b.getContent().setStyle({overflow:b.oldOverflow})}},resetOverflow:function(){this.windows.each(function(b){if(b.oldOverflow){b.getContent().setStyle({overflow:b.oldOverflow})}})},updateZindex:function(b,d){if(b>this.maxZIndex){this.maxZIndex=b;if(this.focusedWindow){this.blur(this.focusedWindow.getId())}}this.focusedWindow=d;if(this.focusedWindow){this.focus(this.focusedWindow.getId())}}};var Dialog={dialogId:null,onCompleteFunc:null,callFunc:null,parameters:null,confirm:function(f,e){if(f&&typeof f!="string"){Dialog._runAjaxRequest(f,e,Dialog.confirm);return}f=f||"";e=e||{};var h=e.okLabel?e.okLabel:"Ok";var b=e.cancelLabel?e.cancelLabel:"Cancel";e=Object.extend(e,e.windowParameters||{});e.windowParameters=e.windowParameters||{};e.className=e.className||"alert";var d="class ='"+(e.buttonClass?e.buttonClass+" ":"")+" ok_button'";var g="class ='"+(e.buttonClass?e.buttonClass+" ":"")+" cancel_button'";var f="
"+f+"
";return this._openDialog(f,e)},alert:function(e,d){if(e&&typeof e!="string"){Dialog._runAjaxRequest(e,d,Dialog.alert);return}e=e||"";d=d||{};var f=d.okLabel?d.okLabel:"Ok";d=Object.extend(d,d.windowParameters||{});d.windowParameters=d.windowParameters||{};d.className=d.className||"alert";var b="class ='"+(d.buttonClass?d.buttonClass+" ":"")+" ok_button'";var e="
"+e+"
";return this._openDialog(e,d)},info:function(d,b){if(d&&typeof d!="string"){Dialog._runAjaxRequest(d,b,Dialog.info);return}d=d||"";b=b||{};b=Object.extend(b,b.windowParameters||{});b.windowParameters=b.windowParameters||{};b.className=b.className||"alert";var d="";if(b.showProgress){d+=""}b.ok=null;b.cancel=null;return this._openDialog(d,b)},setInfoMessage:function(b){$("modal_dialog_message").update(b)},closeInfo:function(){Windows.close(this.dialogId)},_openDialog:function(g,f){var e=f.className;if(!f.height&&!f.width){f.width=WindowUtilities.getPageSize(f.options.parent||document.body).pageWidth/2}if(f.id){this.dialogId=f.id}else{var d=new Date();this.dialogId="modal_dialog_"+d.getTime();f.id=this.dialogId}if(!f.height||!f.width){var b=WindowUtilities._computeSize(g,this.dialogId,f.width,f.height,5,e);if(f.height){f.width=b+5}else{f.height=b+5}}f.effectOptions=f.effectOptions;f.resizable=f.resizable||false;f.minimizable=f.minimizable||false;f.maximizable=f.maximizable||false;f.draggable=f.draggable||false;f.closable=f.closable||false;var h=new Window(f);h.getContent().innerHTML=g;h.showCenter(true,f.top,f.left);h.setDestroyOnClose();h.cancelCallback=f.onCancel||f.cancel;h.okCallback=f.onOk||f.ok;return h},_getAjaxContent:function(b){Dialog.callFunc(b.responseText,Dialog.parameters)},_runAjaxRequest:function(e,d,b){if(e.options==null){e.options={}}Dialog.onCompleteFunc=e.options.onComplete;Dialog.parameters=d;Dialog.callFunc=b;e.options.onComplete=Dialog._getAjaxContent;new Ajax.Request(e.url,e.options)},okCallback:function(){var b=Windows.focusedWindow;if(!b.okCallback||b.okCallback(b)){$$("#"+b.getId()+" input").each(function(d){d.onclick=null});b.close()}},cancelCallback:function(){var b=Windows.focusedWindow;$$("#"+b.getId()+" input").each(function(d){d.onclick=null});b.close();if(b.cancelCallback){b.cancelCallback(b)}}};if(Prototype.Browser.WebKit){var array=navigator.userAgent.match(new RegExp(/AppleWebKit\/([\d\.\+]*)/));Prototype.Browser.WebKitVersion=parseFloat(array[1])}var WindowUtilities={getWindowScroll:function(parent){var T,L,W,H;parent=parent||document.body;if(parent!=document.body){T=parent.scrollTop;L=parent.scrollLeft;W=parent.scrollWidth;H=parent.scrollHeight}else{var w=window;with(w.document){if(w.document.documentElement&&documentElement.scrollTop){T=documentElement.scrollTop;L=documentElement.scrollLeft}else{if(w.document.body){T=body.scrollTop;L=body.scrollLeft}}if(w.innerWidth){W=w.innerWidth;H=w.innerHeight}else{if(w.document.documentElement&&documentElement.clientWidth){W=documentElement.clientWidth;H=documentElement.clientHeight}else{W=body.offsetWidth;H=body.offsetHeight}}}}return{top:T,left:L,width:W,height:H}},getPageSize:function(f){f=f||document.body;var e,l;var g,d;if(f!=document.body){e=f.getWidth();l=f.getHeight();d=f.scrollWidth;g=f.scrollHeight}else{var h,b;if(window.innerHeight&&window.scrollMaxY){h=document.body.scrollWidth;b=window.innerHeight+window.scrollMaxY}else{if(document.body.scrollHeight>document.body.offsetHeight){h=document.body.scrollWidth;b=document.body.scrollHeight}else{h=document.body.offsetWidth;b=document.body.offsetHeight}}if(self.innerHeight){e=document.documentElement.clientWidth;l=self.innerHeight}else{if(document.documentElement&&document.documentElement.clientHeight){e=document.documentElement.clientWidth;l=document.documentElement.clientHeight}else{if(document.body){e=document.body.clientWidth;l=document.body.clientHeight}}}if(b"}catch(h){}var g=d.firstChild||null;if(g&&(g.tagName.toUpperCase()!=b)){g=g.getElementsByTagName(b)[0]}if(!g){g=document.createElement(b)}if(!g){return}if(arguments[1]){if(this._isStringOrNumber(arguments[1])||(arguments[1] instanceof Array)||arguments[1].tagName){this._children(g,arguments[1])}else{var f=this._attributes(arguments[1]);if(f.length){try{d.innerHTML="<"+b+" "+f+">"}catch(h){}g=d.firstChild||null;if(!g){g=document.createElement(b);for(attr in arguments[1]){g[attr=="class"?"className":attr]=arguments[1][attr]}}if(g.tagName.toUpperCase()!=b){g=d.getElementsByTagName(b)[0]}}}}if(arguments[2]){this._children(g,arguments[2])}return $(g)},_text:function(b){return document.createTextNode(b)},ATTR_MAP:{className:"class",htmlFor:"for"},_attributes:function(b){var d=[];for(attribute in b){d.push((attribute in this.ATTR_MAP?this.ATTR_MAP[attribute]:attribute)+'="'+b[attribute].toString().escapeHTML().gsub(/"/,""")+'"')}return d.join(" ")},_children:function(d,b){if(b.tagName){d.appendChild(b);return}if(typeof b=="object"){b.flatten().each(function(f){if(typeof f=="object"){d.appendChild(f)}else{if(Builder._isStringOrNumber(f)){d.appendChild(Builder._text(f))}}})}else{if(Builder._isStringOrNumber(b)){d.appendChild(Builder._text(b))}}},_isStringOrNumber:function(b){return(typeof b=="string"||typeof b=="number")},build:function(d){var b=this.node("div");$(b).update(d.strip());return b.down()},dump:function(d){if(typeof d!="object"&&typeof d!="function"){d=window}var b=("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);b.each(function(e){d[e]=function(){return Builder.node.apply(Builder,[e].concat($A(arguments)))}})}};String.prototype.parseColor=function(){var b="#";if(this.slice(0,4)=="rgb("){var e=this.slice(4,this.length-1).split(",");var d=0;do{b+=parseInt(e[d]).toColorPart()}while(++d<3)}else{if(this.slice(0,1)=="#"){if(this.length==4){for(var d=1;d<4;d++){b+=(this.charAt(d)+this.charAt(d)).toLowerCase()}}if(this.length==7){b=this.toLowerCase()}}}return(b.length==7?b:(arguments[0]||this))};Element.collectTextNodes=function(b){return $A($(b).childNodes).collect(function(d){return(d.nodeType==3?d.nodeValue:(d.hasChildNodes()?Element.collectTextNodes(d):""))}).flatten().join("")};Element.collectTextNodesIgnoreClass=function(b,d){return $A($(b).childNodes).collect(function(e){return(e.nodeType==3?e.nodeValue:((e.hasChildNodes()&&!Element.hasClassName(e,d))?Element.collectTextNodesIgnoreClass(e,d):""))}).flatten().join("")};Element.setContentZoom=function(b,d){b=$(b);b.setStyle({fontSize:(d/100)+"em"});if(Prototype.Browser.WebKit){window.scrollBy(0,0)}return b};Element.getInlineOpacity=function(b){return $(b).style.opacity||""};Element.forceRerendering=function(b){try{b=$(b);var f=document.createTextNode(" ");b.appendChild(f);b.removeChild(f)}catch(d){}};var Effect={_elementDoesNotExistError:{name:"ElementDoesNotExistError",message:"The specified DOM element does not exist, but is required for this effect to operate"},Transitions:{linear:Prototype.K,sinoidal:function(b){return(-Math.cos(b*Math.PI)/2)+0.5},reverse:function(b){return 1-b},flicker:function(b){var b=((-Math.cos(b*Math.PI)/4)+0.75)+Math.random()/4;return b>1?1:b},wobble:function(b){return(-Math.cos(b*Math.PI*(9*b))/2)+0.5},pulse:function(d,b){return(-Math.cos((d*((b||5)-0.5)*2)*Math.PI)/2)+0.5},spring:function(b){return 1-(Math.cos(b*4.5*Math.PI)*Math.exp(-b*6))},none:function(b){return 0},full:function(b){return 1}},DefaultOptions:{duration:1,fps:100,sync:false,from:0,to:1,delay:0,queue:"parallel"},tagifyText:function(b){var d="position:relative";if(Prototype.Browser.IE){d+=";zoom:1"}b=$(b);$A(b.childNodes).each(function(e){if(e.nodeType==3){e.nodeValue.toArray().each(function(f){b.insertBefore(new Element("span",{style:d}).update(f==" "?String.fromCharCode(160):f),e)});Element.remove(e)}})},multiple:function(d,e){var g;if(((typeof d=="object")||Object.isFunction(d))&&(d.length)){g=d}else{g=$(d).childNodes}var b=Object.extend({speed:0.1,delay:0},arguments[2]||{});var f=b.delay;$A(g).each(function(l,h){new e(l,Object.extend(b,{delay:h*b.speed+f}))})},PAIRS:{slide:["SlideDown","SlideUp"],blind:["BlindDown","BlindUp"],appear:["Appear","Fade"]},toggle:function(d,e){d=$(d);e=(e||"appear").toLowerCase();var b=Object.extend({queue:{position:"end",scope:(d.id||"global"),limit:1}},arguments[2]||{});Effect[d.visible()?Effect.PAIRS[e][1]:Effect.PAIRS[e][0]](d,b)}};Effect.DefaultOptions.transition=Effect.Transitions.sinoidal;Effect.ScopedQueue=Class.create(Enumerable,{initialize:function(){this.effects=[];this.interval=null},_each:function(b){this.effects._each(b)},add:function(d){var e=new Date().getTime();var b=Object.isString(d.options.queue)?d.options.queue:d.options.queue.position;switch(b){case"front":this.effects.findAll(function(f){return f.state=="idle"}).each(function(f){f.startOn+=d.finishOn;f.finishOn+=d.finishOn});break;case"with-last":e=this.effects.pluck("startOn").max()||e;break;case"end":e=this.effects.pluck("finishOn").max()||e;break}d.startOn+=e;d.finishOn+=e;if(!d.options.queue.limit||(this.effects.length=this.startOn){if(e>=this.finishOn){this.render(1);this.cancel();this.event("beforeFinish");if(this.finish){this.finish()}this.event("afterFinish");return}var d=(e-this.startOn)/this.totalTime,b=(d*this.totalFrames).round();if(b>this.currentFrame){this.render(d);this.currentFrame=b}}},cancel:function(){if(!this.options.sync){Effect.Queues.get(Object.isString(this.options.queue)?"global":this.options.queue.scope).remove(this)}this.state="finished"},event:function(b){if(this.options[b+"Internal"]){this.options[b+"Internal"](this)}if(this.options[b]){this.options[b](this)}},inspect:function(){var b=$H();for(property in this){if(!Object.isFunction(this[property])){b.set(property,this[property])}}return"#"}});Effect.Parallel=Class.create(Effect.Base,{initialize:function(b){this.effects=b||[];this.start(arguments[1])},update:function(b){this.effects.invoke("render",b)},finish:function(b){this.effects.each(function(d){d.render(1);d.cancel();d.event("beforeFinish");if(d.finish){d.finish(b)}d.event("afterFinish")})}});Effect.Tween=Class.create(Effect.Base,{initialize:function(e,h,g){e=Object.isString(e)?$(e):e;var d=$A(arguments),f=d.last(),b=d.length==5?d[3]:null;this.method=Object.isFunction(f)?f.bind(e):Object.isFunction(e[f])?e[f].bind(e):function(l){e[f]=l};this.start(Object.extend({from:h,to:g},b||{}))},update:function(b){this.method(b)}});Effect.Event=Class.create(Effect.Base,{initialize:function(){this.start(Object.extend({duration:0},arguments[0]||{}))},update:Prototype.emptyFunction});Effect.Opacity=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}if(Prototype.Browser.IE&&(!this.element.currentStyle.hasLayout)){this.element.setStyle({zoom:1})}var b=Object.extend({from:this.element.getOpacity()||0,to:1},arguments[1]||{});this.start(b)},update:function(b){this.element.setOpacity(b)}});Effect.Move=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({x:0,y:0,mode:"relative"},arguments[1]||{});this.start(b)},setup:function(){this.element.makePositioned();this.originalLeft=parseFloat(this.element.getStyle("left")||"0");this.originalTop=parseFloat(this.element.getStyle("top")||"0");if(this.options.mode=="absolute"){this.options.x=this.options.x-this.originalLeft;this.options.y=this.options.y-this.originalTop}},update:function(b){this.element.setStyle({left:(this.options.x*b+this.originalLeft).round()+"px",top:(this.options.y*b+this.originalTop).round()+"px"})}});Effect.MoveBy=function(d,b,e){return new Effect.Move(d,Object.extend({x:e,y:b},arguments[3]||{}))};Effect.Scale=Class.create(Effect.Base,{initialize:function(d,e){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({scaleX:true,scaleY:true,scaleContent:true,scaleFromCenter:false,scaleMode:"box",scaleFrom:100,scaleTo:e},arguments[2]||{});this.start(b)},setup:function(){this.restoreAfterFinish=this.options.restoreAfterFinish||false;this.elementPositioning=this.element.getStyle("position");this.originalStyle={};["top","left","width","height","fontSize"].each(function(d){this.originalStyle[d]=this.element.style[d]}.bind(this));this.originalTop=this.element.offsetTop;this.originalLeft=this.element.offsetLeft;var b=this.element.getStyle("font-size")||"100%";["em","px","%","pt"].each(function(d){if(b.indexOf(d)>0){this.fontSize=parseFloat(b);this.fontSizeType=d}}.bind(this));this.factor=(this.options.scaleTo-this.options.scaleFrom)/100;this.dims=null;if(this.options.scaleMode=="box"){this.dims=[this.element.offsetHeight,this.element.offsetWidth]}if(/^content/.test(this.options.scaleMode)){this.dims=[this.element.scrollHeight,this.element.scrollWidth]}if(!this.dims){this.dims=[this.options.scaleMode.originalHeight,this.options.scaleMode.originalWidth]}},update:function(b){var d=(this.options.scaleFrom/100)+(this.factor*b);if(this.options.scaleContent&&this.fontSize){this.element.setStyle({fontSize:this.fontSize*d+this.fontSizeType})}this.setDimensions(this.dims[0]*d,this.dims[1]*d)},finish:function(b){if(this.restoreAfterFinish){this.element.setStyle(this.originalStyle)}},setDimensions:function(b,g){var h={};if(this.options.scaleX){h.width=g.round()+"px"}if(this.options.scaleY){h.height=b.round()+"px"}if(this.options.scaleFromCenter){var f=(b-this.dims[0])/2;var e=(g-this.dims[1])/2;if(this.elementPositioning=="absolute"){if(this.options.scaleY){h.top=this.originalTop-f+"px"}if(this.options.scaleX){h.left=this.originalLeft-e+"px"}}else{if(this.options.scaleY){h.top=-f+"px"}if(this.options.scaleX){h.left=-e+"px"}}}this.element.setStyle(h)}});Effect.Highlight=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({startcolor:"#ffff99"},arguments[1]||{});this.start(b)},setup:function(){if(this.element.getStyle("display")=="none"){this.cancel();return}this.oldStyle={};if(!this.options.keepBackgroundImage){this.oldStyle.backgroundImage=this.element.getStyle("background-image");this.element.setStyle({backgroundImage:"none"})}if(!this.options.endcolor){this.options.endcolor=this.element.getStyle("background-color").parseColor("#ffffff")}if(!this.options.restorecolor){this.options.restorecolor=this.element.getStyle("background-color")}this._base=$R(0,2).map(function(b){return parseInt(this.options.startcolor.slice(b*2+1,b*2+3),16)}.bind(this));this._delta=$R(0,2).map(function(b){return parseInt(this.options.endcolor.slice(b*2+1,b*2+3),16)-this._base[b]}.bind(this))},update:function(b){this.element.setStyle({backgroundColor:$R(0,2).inject("#",function(d,e,f){return d+((this._base[f]+(this._delta[f]*b)).round().toColorPart())}.bind(this))})},finish:function(){this.element.setStyle(Object.extend(this.oldStyle,{backgroundColor:this.options.restorecolor}))}});Effect.ScrollTo=function(e){var d=arguments[1]||{},b=document.viewport.getScrollOffsets(),f=$(e).cumulativeOffset();if(d.offset){f[1]+=d.offset}return new Effect.Tween(null,b.top,f[1],d,function(g){scrollTo(b.left,g.round())})};Effect.Fade=function(e){e=$(e);var b=e.getInlineOpacity();var d=Object.extend({from:e.getOpacity()||1,to:0,afterFinishInternal:function(f){if(f.options.to!=0){return}f.element.hide().setStyle({opacity:b})}},arguments[1]||{});return new Effect.Opacity(e,d)};Effect.Appear=function(d){d=$(d);var b=Object.extend({from:(d.getStyle("display")=="none"?0:d.getOpacity()||0),to:1,afterFinishInternal:function(e){e.element.forceRerendering()},beforeSetup:function(e){e.element.setOpacity(e.options.from).show()}},arguments[1]||{});return new Effect.Opacity(d,b)};Effect.Puff=function(d){d=$(d);var b={opacity:d.getInlineOpacity(),position:d.getStyle("position"),top:d.style.top,left:d.style.left,width:d.style.width,height:d.style.height};return new Effect.Parallel([new Effect.Scale(d,200,{sync:true,scaleFromCenter:true,scaleContent:true,restoreAfterFinish:true}),new Effect.Opacity(d,{sync:true,to:0})],Object.extend({duration:1,beforeSetupInternal:function(e){Position.absolutize(e.effects[0].element)},afterFinishInternal:function(e){e.effects[0].element.hide().setStyle(b)}},arguments[1]||{}))};Effect.BlindUp=function(b){b=$(b);b.makeClipping();return new Effect.Scale(b,0,Object.extend({scaleContent:false,scaleX:false,restoreAfterFinish:true,afterFinishInternal:function(d){d.element.hide().undoClipping()}},arguments[1]||{}))};Effect.BlindDown=function(d){d=$(d);var b=d.getDimensions();return new Effect.Scale(d,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:b.height,originalWidth:b.width},restoreAfterFinish:true,afterSetup:function(e){e.element.makeClipping().setStyle({height:"0px"}).show()},afterFinishInternal:function(e){e.element.undoClipping()}},arguments[1]||{}))};Effect.SwitchOff=function(d){d=$(d);var b=d.getInlineOpacity();return new Effect.Appear(d,Object.extend({duration:0.4,from:0,transition:Effect.Transitions.flicker,afterFinishInternal:function(e){new Effect.Scale(e.element,1,{duration:0.3,scaleFromCenter:true,scaleX:false,scaleContent:false,restoreAfterFinish:true,beforeSetup:function(f){f.element.makePositioned().makeClipping()},afterFinishInternal:function(f){f.element.hide().undoClipping().undoPositioned().setStyle({opacity:b})}})}},arguments[1]||{}))};Effect.DropOut=function(d){d=$(d);var b={top:d.getStyle("top"),left:d.getStyle("left"),opacity:d.getInlineOpacity()};return new Effect.Parallel([new Effect.Move(d,{x:0,y:100,sync:true}),new Effect.Opacity(d,{sync:true,to:0})],Object.extend({duration:0.5,beforeSetup:function(e){e.effects[0].element.makePositioned()},afterFinishInternal:function(e){e.effects[0].element.hide().undoPositioned().setStyle(b)}},arguments[1]||{}))};Effect.Shake=function(f){f=$(f);var d=Object.extend({distance:20,duration:0.5},arguments[1]||{});var g=parseFloat(d.distance);var e=parseFloat(d.duration)/10;var b={top:f.getStyle("top"),left:f.getStyle("left")};return new Effect.Move(f,{x:g,y:0,duration:e,afterFinishInternal:function(h){new Effect.Move(h.element,{x:-g*2,y:0,duration:e*2,afterFinishInternal:function(l){new Effect.Move(l.element,{x:g*2,y:0,duration:e*2,afterFinishInternal:function(n){new Effect.Move(n.element,{x:-g*2,y:0,duration:e*2,afterFinishInternal:function(o){new Effect.Move(o.element,{x:g*2,y:0,duration:e*2,afterFinishInternal:function(p){new Effect.Move(p.element,{x:-g,y:0,duration:e,afterFinishInternal:function(q){q.element.undoPositioned().setStyle(b)}})}})}})}})}})}})};Effect.SlideDown=function(e){e=$(e).cleanWhitespace();var b=e.down().getStyle("bottom");var d=e.getDimensions();return new Effect.Scale(e,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:window.opera?0:1,scaleMode:{originalHeight:d.height,originalWidth:d.width},restoreAfterFinish:true,afterSetup:function(f){f.element.makePositioned();f.element.down().makePositioned();if(window.opera){f.element.setStyle({top:""})}f.element.makeClipping().setStyle({height:"0px"}).show()},afterUpdateInternal:function(f){f.element.down().setStyle({bottom:(f.dims[0]-f.element.clientHeight)+"px"})},afterFinishInternal:function(f){f.element.undoClipping().undoPositioned();f.element.down().undoPositioned().setStyle({bottom:b})}},arguments[1]||{}))};Effect.SlideUp=function(e){e=$(e).cleanWhitespace();var b=e.down().getStyle("bottom");var d=e.getDimensions();return new Effect.Scale(e,window.opera?0:1,Object.extend({scaleContent:false,scaleX:false,scaleMode:"box",scaleFrom:100,scaleMode:{originalHeight:d.height,originalWidth:d.width},restoreAfterFinish:true,afterSetup:function(f){f.element.makePositioned();f.element.down().makePositioned();if(window.opera){f.element.setStyle({top:""})}f.element.makeClipping().show()},afterUpdateInternal:function(f){f.element.down().setStyle({bottom:(f.dims[0]-f.element.clientHeight)+"px"})},afterFinishInternal:function(f){f.element.hide().undoClipping().undoPositioned();f.element.down().undoPositioned().setStyle({bottom:b})}},arguments[1]||{}))};Effect.Squish=function(b){return new Effect.Scale(b,window.opera?1:0,{restoreAfterFinish:true,beforeSetup:function(d){d.element.makeClipping()},afterFinishInternal:function(d){d.element.hide().undoClipping()}})};Effect.Grow=function(e){e=$(e);var d=Object.extend({direction:"center",moveTransition:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.full},arguments[1]||{});var b={top:e.style.top,left:e.style.left,height:e.style.height,width:e.style.width,opacity:e.getInlineOpacity()};var l=e.getDimensions();var n,h;var g,f;switch(d.direction){case"top-left":n=h=g=f=0;break;case"top-right":n=l.width;h=f=0;g=-l.width;break;case"bottom-left":n=g=0;h=l.height;f=-l.height;break;case"bottom-right":n=l.width;h=l.height;g=-l.width;f=-l.height;break;case"center":n=l.width/2;h=l.height/2;g=-l.width/2;f=-l.height/2;break}return new Effect.Move(e,{x:n,y:h,duration:0.01,beforeSetup:function(o){o.element.hide().makeClipping().makePositioned()},afterFinishInternal:function(o){new Effect.Parallel([new Effect.Opacity(o.element,{sync:true,to:1,from:0,transition:d.opacityTransition}),new Effect.Move(o.element,{x:g,y:f,sync:true,transition:d.moveTransition}),new Effect.Scale(o.element,100,{scaleMode:{originalHeight:l.height,originalWidth:l.width},sync:true,scaleFrom:window.opera?1:0,transition:d.scaleTransition,restoreAfterFinish:true})],Object.extend({beforeSetup:function(p){p.effects[0].element.setStyle({height:"0px"}).show()},afterFinishInternal:function(p){p.effects[0].element.undoClipping().undoPositioned().setStyle(b)}},d))}})};Effect.Shrink=function(e){e=$(e);var d=Object.extend({direction:"center",moveTransition:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.none},arguments[1]||{});var b={top:e.style.top,left:e.style.left,height:e.style.height,width:e.style.width,opacity:e.getInlineOpacity()};var h=e.getDimensions();var g,f;switch(d.direction){case"top-left":g=f=0;break;case"top-right":g=h.width;f=0;break;case"bottom-left":g=0;f=h.height;break;case"bottom-right":g=h.width;f=h.height;break;case"center":g=h.width/2;f=h.height/2;break}return new Effect.Parallel([new Effect.Opacity(e,{sync:true,to:0,from:1,transition:d.opacityTransition}),new Effect.Scale(e,window.opera?1:0,{sync:true,transition:d.scaleTransition,restoreAfterFinish:true}),new Effect.Move(e,{x:g,y:f,sync:true,transition:d.moveTransition})],Object.extend({beforeStartInternal:function(l){l.effects[0].element.makePositioned().makeClipping()},afterFinishInternal:function(l){l.effects[0].element.hide().undoClipping().undoPositioned().setStyle(b)}},d))};Effect.Pulsate=function(e){e=$(e);var d=arguments[1]||{},b=e.getInlineOpacity(),g=d.transition||Effect.Transitions.linear,f=function(h){return 1-g((-Math.cos((h*(d.pulses||5)*2)*Math.PI)/2)+0.5)};return new Effect.Opacity(e,Object.extend(Object.extend({duration:2,from:0,afterFinishInternal:function(h){h.element.setStyle({opacity:b})}},d),{transition:f}))};Effect.Fold=function(d){d=$(d);var b={top:d.style.top,left:d.style.left,width:d.style.width,height:d.style.height};d.makeClipping();return new Effect.Scale(d,5,Object.extend({scaleContent:false,scaleX:false,afterFinishInternal:function(e){new Effect.Scale(d,1,{scaleContent:false,scaleY:false,afterFinishInternal:function(f){f.element.hide().undoClipping().setStyle(b)}})}},arguments[1]||{}))};Effect.Morph=Class.create(Effect.Base,{initialize:function(e){this.element=$(e);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({style:{}},arguments[1]||{});if(!Object.isString(b.style)){this.style=$H(b.style)}else{if(b.style.include(":")){this.style=b.style.parseStyle()}else{this.element.addClassName(b.style);this.style=$H(this.element.getStyles());this.element.removeClassName(b.style);var d=this.element.getStyles();this.style=this.style.reject(function(f){return f.value==d[f.key]});b.afterFinishInternal=function(f){f.element.addClassName(f.options.style);f.transforms.each(function(g){f.element.style[g.style]=""})}}}this.start(b)},setup:function(){function b(d){if(!d||["rgba(0, 0, 0, 0)","transparent"].include(d)){d="#ffffff"}d=d.parseColor();return $R(0,2).map(function(e){return parseInt(d.slice(e*2+1,e*2+3),16)})}this.transforms=this.style.map(function(l){var h=l[0],g=l[1],f=null;if(g.parseColor("#zzzzzz")!="#zzzzzz"){g=g.parseColor();f="color"}else{if(h=="opacity"){g=parseFloat(g);if(Prototype.Browser.IE&&(!this.element.currentStyle.hasLayout)){this.element.setStyle({zoom:1})}}else{if(Element.CSS_LENGTH.test(g)){var e=g.match(/^([\+\-]?[0-9\.]+)(.*)$/);g=parseFloat(e[1]);f=(e.length==3)?e[2]:null}}}var d=this.element.getStyle(h);return{style:h.camelize(),originalValue:f=="color"?b(d):parseFloat(d||0),targetValue:f=="color"?b(g):g,unit:f}}.bind(this)).reject(function(d){return((d.originalValue==d.targetValue)||(d.unit!="color"&&(isNaN(d.originalValue)||isNaN(d.targetValue))))})},update:function(b){var f={},d,e=this.transforms.length;while(e--){f[(d=this.transforms[e]).style]=d.unit=="color"?"#"+(Math.round(d.originalValue[0]+(d.targetValue[0]-d.originalValue[0])*b)).toColorPart()+(Math.round(d.originalValue[1]+(d.targetValue[1]-d.originalValue[1])*b)).toColorPart()+(Math.round(d.originalValue[2]+(d.targetValue[2]-d.originalValue[2])*b)).toColorPart():(d.originalValue+(d.targetValue-d.originalValue)*b).toFixed(3)+(d.unit===null?"":d.unit)}this.element.setStyle(f,true)}});Effect.Transform=Class.create({initialize:function(b){this.tracks=[];this.options=arguments[1]||{};this.addTracks(b)},addTracks:function(b){b.each(function(d){d=$H(d);var e=d.values().first();this.tracks.push($H({ids:d.keys().first(),effect:Effect.Morph,options:{style:e}}))}.bind(this));return this},play:function(){return new Effect.Parallel(this.tracks.map(function(b){var f=b.get("ids"),e=b.get("effect"),d=b.get("options");var g=[$(f)||$$(f)].flatten();return g.map(function(h){return new e(h,Object.extend({sync:true},d))})}).flatten(),this.options)}});Element.CSS_PROPERTIES=$w("backgroundColor backgroundPosition borderBottomColor borderBottomStyle borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth borderRightColor borderRightStyle borderRightWidth borderSpacing borderTopColor borderTopStyle borderTopWidth bottom clip color fontSize fontWeight height left letterSpacing lineHeight marginBottom marginLeft marginRight marginTop markerOffset maxHeight maxWidth minHeight minWidth opacity outlineColor outlineOffset outlineWidth paddingBottom paddingLeft paddingRight paddingTop right textIndent top width wordSpacing zIndex");Element.CSS_LENGTH=/^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;String.__parseStyleElement=document.createElement("div");String.prototype.parseStyle=function(){var d,b=$H();if(Prototype.Browser.WebKit){d=new Element("div",{style:this}).style}else{String.__parseStyleElement.innerHTML='
';d=String.__parseStyleElement.childNodes[0].style}Element.CSS_PROPERTIES.each(function(e){if(d[e]){b.set(e,d[e])}});if(Prototype.Browser.IE&&this.include("opacity")){b.set("opacity",this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1])}return b};if(document.defaultView&&document.defaultView.getComputedStyle){Element.getStyles=function(d){var b=document.defaultView.getComputedStyle($(d),null);return Element.CSS_PROPERTIES.inject({},function(e,f){e[f]=b[f];return e})}}else{Element.getStyles=function(d){d=$(d);var b=d.currentStyle,e;e=Element.CSS_PROPERTIES.inject({},function(f,g){f[g]=b[g];return f});if(!e.opacity){e.opacity=d.getOpacity()}return e}}Effect.Methods={morph:function(b,d){b=$(b);new Effect.Morph(b,Object.extend({style:d},arguments[2]||{}));return b},visualEffect:function(e,g,d){e=$(e);var f=g.dasherize().camelize(),b=f.charAt(0).toUpperCase()+f.substring(1);new Effect[b](e,d);return e},highlight:function(d,b){d=$(d);new Effect.Highlight(d,b);return d}};$w("fade appear grow shrink fold blindUp blindDown slideUp slideDown pulsate shake puff squish switchOff dropOut").each(function(b){Effect.Methods[b]=function(e,d){e=$(e);Effect[b.charAt(0).toUpperCase()+b.substring(1)](e,d);return e}});$w("getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles").each(function(b){Effect.Methods[b]=Element[b]});Element.addMethods(Effect.Methods);function validateCreditCard(e){var d="0123456789";var b="";for(i=0;i9?Math.floor(a/10+a%10):a}for(i=0;i=d},maxLength:function(b,e,d){return b.length<=d},min:function(b,e,d){return b>=parseFloat(d)},max:function(b,e,d){return b<=parseFloat(d)},notOneOf:function(b,e,d){return $A(d).all(function(f){return b!=f})},oneOf:function(b,e,d){return $A(d).any(function(f){return b==f})},is:function(b,e,d){return b==d},isNot:function(b,e,d){return b!=d},equalToField:function(b,e,d){return b==$F(d)},notEqualToField:function(b,e,d){return b!=$F(d)},include:function(b,e,d){return $A(d).all(function(f){return Validation.get(f).test(b,e)})}};var Validation=Class.create();Validation.defaultOptions={onSubmit:true,stopOnFirst:false,immediate:false,focusOnError:true,useTitles:false,addClassNameToContainer:false,containerClassName:".input-box",onFormValidate:function(b,d){},onElementValidate:function(b,d){}};Validation.prototype={initialize:function(d,b){this.form=$(d);if(!this.form){return}this.options=Object.extend({onSubmit:Validation.defaultOptions.onSubmit,stopOnFirst:Validation.defaultOptions.stopOnFirst,immediate:Validation.defaultOptions.immediate,focusOnError:Validation.defaultOptions.focusOnError,useTitles:Validation.defaultOptions.useTitles,onFormValidate:Validation.defaultOptions.onFormValidate,onElementValidate:Validation.defaultOptions.onElementValidate},b||{});if(this.options.onSubmit){Event.observe(this.form,"submit",this.onSubmit.bind(this),false)}if(this.options.immediate){Form.getElements(this.form).each(function(e){if(e.tagName.toLowerCase()=="select"){Event.observe(e,"blur",this.onChange.bindAsEventListener(this))}if(e.type.toLowerCase()=="radio"||e.type.toLowerCase()=="checkbox"){Event.observe(e,"click",this.onChange.bindAsEventListener(this))}else{Event.observe(e,"change",this.onChange.bindAsEventListener(this))}},this)}},onChange:function(b){Validation.isOnChange=true;Validation.validate(Event.element(b),{useTitle:this.options.useTitles,onElementValidate:this.options.onElementValidate});Validation.isOnChange=false},onSubmit:function(b){if(!this.validate()){Event.stop(b)}},validate:function(){var b=false;var d=this.options.useTitles;var g=this.options.onElementValidate;try{if(this.options.stopOnFirst){b=Form.getElements(this.form).all(function(e){if(e.hasClassName("local-validation")&&!this.isElementInForm(e,this.form)){return true}return Validation.validate(e,{useTitle:d,onElementValidate:g})},this)}else{b=Form.getElements(this.form).collect(function(e){if(e.hasClassName("local-validation")&&!this.isElementInForm(e,this.form)){return true}if(e.hasClassName("validation-disabled")){return true}return Validation.validate(e,{useTitle:d,onElementValidate:g})},this).all()}}catch(f){}if(!b&&this.options.focusOnError){try{Form.getElements(this.form).findAll(function(e){return $(e).hasClassName("validation-failed")}).first().focus()}catch(f){}}this.options.onFormValidate(b,this.form);return b},reset:function(){Form.getElements(this.form).each(Validation.reset)},isElementInForm:function(e,d){var b=e.up("form");if(b==d){return true}return false}};Object.extend(Validation,{validate:function(e,b){b=Object.extend({useTitle:false,onElementValidate:function(f,g){}},b||{});e=$(e);var d=$w(e.className);return result=d.all(function(f){var g=Validation.test(f,e,b.useTitle);b.onElementValidate(g,e);return g})},insertAdvice:function(f,d){var b=$(f).up(".field-row");if(b){Element.insert(b,{after:d})}else{if(f.up("td.value")){f.up("td.value").insert({bottom:d})}else{if(f.advaiceContainer&&$(f.advaiceContainer)){$(f.advaiceContainer).update(d)}else{switch(f.type.toLowerCase()){case"checkbox":case"radio":var e=f.parentNode;if(e){Element.insert(e,{bottom:d})}else{Element.insert(f,{after:d})}break;default:Element.insert(f,{after:d})}}}}},showAdvice:function(e,d,b){if(!e.advices){e.advices=new Hash()}else{e.advices.each(function(f){if(!d||f.value.id!=d.id){this.hideAdvice(e,f.value)}}.bind(this))}e.advices.set(b,d);if(typeof Effect=="undefined"){d.style.display="block"}else{if(!d._adviceAbsolutize){new Effect.Appear(d,{duration:1})}else{Position.absolutize(d);d.show();d.setStyle({top:d._adviceTop,left:d._adviceLeft,width:d._adviceWidth,"z-index":1000});d.addClassName("advice-absolute")}}},hideAdvice:function(d,b){if(b!=null){new Effect.Fade(b,{duration:1,afterFinishInternal:function(){b.hide()}})}},updateCallback:function(elm,status){if(typeof elm.callbackFunction!="undefined"){eval(elm.callbackFunction+"('"+elm.id+"','"+status+"')")}},ajaxError:function(g,f){var e="validate-ajax";var d=Validation.getAdvice(e,g);if(d==null){d=this.createAdvice(e,g,false,f)}this.showAdvice(g,d,"validate-ajax");this.updateCallback(g,"failed");g.addClassName("validation-failed");g.addClassName("validate-ajax");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=g.up(Validation.defaultOptions.containerClassName);if(b&&this.allowContainerClassName(g)){b.removeClassName("validation-passed");b.addClassName("validation-error")}}},allowContainerClassName:function(b){if(b.type=="radio"||b.type=="checkbox"){return b.hasClassName("change-container-classname")}return true},test:function(g,o,l){var d=Validation.get(g);var n="__advice"+g.camelize();try{if(Validation.isVisible(o)&&!d.test($F(o),o)){var f=Validation.getAdvice(g,o);if(f==null){f=this.createAdvice(g,o,l)}this.showAdvice(o,f,g);this.updateCallback(o,"failed");o[n]=1;if(!o.advaiceContainer){o.removeClassName("validation-passed");o.addClassName("validation-failed")}if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=o.up(Validation.defaultOptions.containerClassName);if(b&&this.allowContainerClassName(o)){b.removeClassName("validation-passed");b.addClassName("validation-error")}}return false}else{var f=Validation.getAdvice(g,o);this.hideAdvice(o,f);this.updateCallback(o,"passed");o[n]="";o.removeClassName("validation-failed");o.addClassName("validation-passed");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=o.up(Validation.defaultOptions.containerClassName);if(b&&!b.down(".validation-failed")&&this.allowContainerClassName(o)){if(!Validation.get("IsEmpty").test(o.value)||!this.isVisible(o)){b.addClassName("validation-passed")}else{b.removeClassName("validation-passed")}b.removeClassName("validation-error")}}return true}}catch(h){throw (h)}},isVisible:function(b){while(b.tagName!="BODY"){if(!$(b).visible()){return false}b=b.parentNode}return true},getAdvice:function(b,d){return $("advice-"+b+"-"+Validation.getElmID(d))||$("advice-"+Validation.getElmID(d))},createAdvice:function(e,n,l,d){var b=Validation.get(e);var h=l?((n&&n.title)?n.title:b.error):b.error;if(d){h=d}if(jQuery.mage.__){h=jQuery.mage.__(h)}advice='";Validation.insertAdvice(n,advice);advice=Validation.getAdvice(e,n);if($(n).hasClassName("absolute-advice")){var g=$(n).getDimensions();var f=Position.cumulativeOffset(n);advice._adviceTop=(f[1]+g.height)+"px";advice._adviceLeft=(f[0])+"px";advice._adviceWidth=(g.width)+"px";advice._adviceAbsolutize=true}return advice},getElmID:function(b){return b.id?b.id:b.name},reset:function(d){d=$(d);var b=$w(d.className);b.each(function(g){var h="__advice"+g.camelize();if(d[h]){var f=Validation.getAdvice(g,d);if(f){f.hide()}d[h]=""}d.removeClassName("validation-failed");d.removeClassName("validation-passed");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var e=d.up(Validation.defaultOptions.containerClassName);if(e){e.removeClassName("validation-passed");e.removeClassName("validation-error")}}})},add:function(f,e,g,d){var b={};b[f]=new Validator(f,e,g,d);Object.extend(Validation.methods,b)},addAllThese:function(b){var d={};$A(b).each(function(e){d[e[0]]=new Validator(e[0],e[1],e[2],(e.length>3?e[3]:{}))});Object.extend(Validation.methods,d)},get:function(b){return Validation.methods[b]?Validation.methods[b]:Validation.methods._LikeNoIDIEverSaw_},methods:{_LikeNoIDIEverSaw_:new Validator("_LikeNoIDIEverSaw_","",{})}});Validation.add("IsEmpty","",function(b){return(b==""||(b==null)||(b.length==0)||/^\s+$/.test(b))});Validation.addAllThese([["validate-no-html-tags","HTML tags are not allowed",function(b){return !/<(\/)?\w+/.test(b)}],["validate-select","Please select an option.",function(b){return((b!="none")&&(b!=null)&&(b.length!=0))}],["required-entry","This is a required field.",function(b){return !Validation.get("IsEmpty").test(b)}],["validate-number","Please enter a valid number in this field.",function(b){return Validation.get("IsEmpty").test(b)||(!isNaN(parseNumber(b))&&/^\s*-?\d*(\.\d*)?\s*$/.test(b))}],["validate-number-range","The value is not within the specified range.",function(e,g){if(Validation.get("IsEmpty").test(e)){return true}var f=parseNumber(e);if(isNaN(f)){return false}var d=/^number-range-(-?[\d.,]+)?-(-?[\d.,]+)?$/,b=true;$w(g.className).each(function(l){var h=d.exec(l);if(h){b=b&&(h[1]==null||h[1]==""||f>=parseNumber(h[1]))&&(h[2]==null||h[2]==""||f<=parseNumber(h[2]))}});return b}],["validate-digits","Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.",function(b){return Validation.get("IsEmpty").test(b)||!/[^\d]/.test(b)}],["validate-digits-range","The value is not within the specified range.",function(e,g){if(Validation.get("IsEmpty").test(e)){return true}var f=parseNumber(e);if(isNaN(f)){return false}var d=/^digits-range-(-?\d+)?-(-?\d+)?$/,b=true;$w(g.className).each(function(l){var h=d.exec(l);if(h){b=b&&(h[1]==null||h[1]==""||f>=parseNumber(h[1]))&&(h[2]==null||h[2]==""||f<=parseNumber(h[2]))}});return b}],["validate-range","The value is not within the specified range.",function(f,l){var g,h;if(Validation.get("IsEmpty").test(f)){return true}else{if(Validation.get("validate-digits").test(f)){g=h=parseNumber(f)}else{var e=/^(-?\d+)?-(-?\d+)?$/.exec(f);if(e){g=parseNumber(e[1]);h=parseNumber(e[2]);if(g>h){return false}}else{return false}}}var d=/^range-(-?\d+)?-(-?\d+)?$/,b=true;$w(l.className).each(function(n){var q=d.exec(n);if(q){var p=parseNumber(q[1]);var o=parseNumber(q[2]);b=b&&(isNaN(p)||g>=p)&&(isNaN(o)||h<=o)}});return b}],["validate-alpha","Please use letters only (a-z or A-Z) in this field.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z]+$/.test(b)}],["validate-code","Please use only letters (a-z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-z]+[a-z0-9_]+$/.test(b)}],["validate-alphanum","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z0-9]+$/.test(b)}],["validate-alphanum-with-spaces","Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z0-9 ]+$/.test(b)}],["validate-street",'Please use only letters (a-z or A-Z), numbers (0-9), spaces and "#" in this field.',function(b){return Validation.get("IsEmpty").test(b)||/^[ \w]{3,}([A-Za-z]\.)?([ \w]*\#\d+)?(\r\n| )[ \w]{3,}/.test(b)}],["validate-phoneStrict","Please enter a valid phone number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^(\()?\d{3}(\))?(-|\s)?\d{3}(-|\s)\d{4}$/.test(b)}],["validate-phoneLax","Please enter a valid phone number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^((\d[-. ]?)?((\(\d{3}\))|\d{3}))?[-. ]?\d{3}[-. ]?\d{4}$/.test(b)}],["validate-fax","Please enter a valid fax number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^(\()?\d{3}(\))?(-|\s)?\d{3}(-|\s)\d{4}$/.test(b)}],["validate-date","Please enter a valid date.",function(b){var d=new Date(b);return Validation.get("IsEmpty").test(b)||!isNaN(d)}],["validate-date-range","Make sure the To Date is later than or the same as the From Date.",function(e,h){var d=/\bdate-range-(\w+)-(\w+)\b/.exec(h.className);if(!d||d[2]=="to"||Validation.get("IsEmpty").test(e)){return true}var f=new Date().getFullYear()+"";var b=function(l){l=l.split(/[.\/]/);if(l[2]&&l[2].length<4){l[2]=f.substr(0,l[2].length)+l[2]}return new Date(l.join("/")).getTime()};var g=Element.select(h.form,".validate-date-range.date-range-"+d[1]+"-to");return !g.length||Validation.get("IsEmpty").test(g[0].value)||b(e)<=b(g[0].value)}],["validate-email","Please enter a valid email address (Ex: johndoe@domain.com).",function(b){return Validation.get("IsEmpty").test(b)||/^([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*@([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*\.(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,})$/i.test(b)}],["validate-emailSender","Please use only visible characters and spaces.",function(b){return Validation.get("IsEmpty").test(b)||/^[\S ]+$/.test(b)}],["validate-password","Please enter 6 or more characters. Leading or trailing spaces will be ignored.",function(b){var d=b.strip();return !(d.length>0&&d.length<6)}],["validate-admin-password","Please enter 7 or more characters, using both numeric and alphabetic.",function(b){var d=b.strip();if(0==d.length){return true}if(!(/[a-z]/i.test(b))||!(/[0-9]/.test(b))){return false}return !(d.length<7)}],["validate-cpassword","Please make sure your passwords match.",function(b){var d=$("confirmation")?$("confirmation"):$$(".validate-cpassword")[0];var g=false;if($("password")){g=$("password")}var h=$$(".validate-password");for(var e=0;e=0}],["validate-zero-or-greater","Please enter a number 0 or greater in this field.",function(b){return Validation.get("validate-not-negative-number").test(b)}],["validate-greater-than-zero","Please enter a number greater than 0 in this field.",function(b){if(Validation.get("IsEmpty").test(b)){return true}b=parseNumber(b);return !isNaN(b)&&b>0}],["validate-state","Please select State/Province.",function(b){return(b!=0||b=="")}],["validate-new-password","Please enter 6 or more characters. Leading or trailing spaces will be ignored.",function(b){if(!Validation.get("validate-password").test(b)){return false}if(Validation.get("IsEmpty").test(b)&&b!=""){return false}return true}],["validate-cc-number","Please enter a valid credit card number.",function(b,e){var d=$(e.id.substr(0,e.id.indexOf("_cc_number"))+"_cc_type");if(d&&typeof Validation.creditCartTypes.get(d.value)!="undefined"&&Validation.creditCartTypes.get(d.value)[2]==false){if(!Validation.get("IsEmpty").test(b)&&Validation.get("validate-digits").test(b)){return true}else{return false}}return validateCreditCard(b)}],["validate-cc-type","Credit card number does not match credit card type.",function(d,g){g.value=removeDelimiters(g.value);d=removeDelimiters(d);var f=$(g.id.substr(0,g.id.indexOf("_cc_number"))+"_cc_type");if(!f){return true}var e=f.value;if(typeof Validation.creditCartTypes.get(e)=="undefined"){return false}if(Validation.creditCartTypes.get(e)[0]==false){return true}var b="";Validation.creditCartTypes.each(function(h){if(h.value[0]&&d.match(h.value[0])){b=h.key;throw $break}});if(b!=e){return false}if(f.hasClassName("validation-failed")&&Validation.isOnChange){Validation.validate(f)}return true}],["validate-cc-type-select","Card type does not match credit card number.",function(d,e){var b=$(e.id.substr(0,e.id.indexOf("_cc_type"))+"_cc_number");if(Validation.isOnChange&&Validation.get("IsEmpty").test(b.value)){return true}if(Validation.get("validate-cc-type").test(b.value,b)){Validation.validate(b)}return Validation.get("validate-cc-type").test(b.value,b)}],["validate-cc-exp","Incorrect credit card expiration date.",function(b,l){var h=b;var g=$(l.id.substr(0,l.id.indexOf("_expiration"))+"_expiration_yr").value;var f=new Date();var e=f.getMonth()+1;var d=f.getFullYear();if(h=n)}});return b}],["validate-percents","Please enter a number lower than 100.",{max:100}],["required-file","Please select a file.",function(d,e){var b=!Validation.get("IsEmpty").test(d);if(b===false){ovId=e.id+"_value";if($(ovId)){b=!Validation.get("IsEmpty").test($(ovId).value)}}return b}],["validate-cc-ukss","Please enter issue number or start date for switch/solo card type.",function(o,g){var b;if(g.id.match(/(.)+_cc_issue$/)){b=g.id.indexOf("_cc_issue")}else{if(g.id.match(/(.)+_start_month$/)){b=g.id.indexOf("_start_month")}else{b=g.id.indexOf("_start_year")}}var f=g.id.substr(0,b);var d=$(f+"_cc_type");if(!d){return true}var n=d.value;if(["SS","SM","SO"].indexOf(n)==-1){return true}$(f+"_cc_issue").advaiceContainer=$(f+"_start_month").advaiceContainer=$(f+"_start_year").advaiceContainer=$(f+"_cc_type_ss_div").down(".adv-container");var h=$(f+"_cc_issue").value;var l=$(f+"_start_month").value;var p=$(f+"_start_year").value;var e=(l&&p)?true:false;if(!e&&!h){return false}return true}]]);function removeDelimiters(b){b=b.replace(/\s/g,"");b=b.replace(/\-/g,"");return b}function parseNumber(b){if(typeof b!="string"){return parseFloat(b)}var e=b.indexOf(".");var d=b.indexOf(",");if(e!=-1&&d!=-1){if(d>e){b=b.replace(".","").replace(",",".")}else{b=b.replace(",","")}}else{if(d!=-1){b=b.replace(",",".")}}return parseFloat(b)}Validation.creditCartTypes=$H({SO:[new RegExp("^(6334[5-9]([0-9]{11}|[0-9]{13,14}))|(6767([0-9]{12}|[0-9]{14,15}))$"),new RegExp("^([0-9]{3}|[0-9]{4})?$"),true],SM:[new RegExp("(^(5[0678])[0-9]{11,18}$)|(^(6[^05])[0-9]{11,18}$)|(^(601)[^1][0-9]{9,16}$)|(^(6011)[0-9]{9,11}$)|(^(6011)[0-9]{13,16}$)|(^(65)[0-9]{11,13}$)|(^(65)[0-9]{15,18}$)|(^(49030)[2-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49033)[5-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49110)[1-2]([0-9]{10}$|[0-9]{12,13}$))|(^(49117)[4-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49118)[0-2]([0-9]{10}$|[0-9]{12,13}$))|(^(4936)([0-9]{12}$|[0-9]{14,15}$))"),new RegExp("^([0-9]{3}|[0-9]{4})?$"),true],VI:[new RegExp("^4[0-9]{12}([0-9]{3})?$"),new RegExp("^[0-9]{3}$"),true],MC:[new RegExp("^5[1-5][0-9]{14}$"),new RegExp("^[0-9]{3}$"),true],AE:[new RegExp("^3[47][0-9]{13}$"),new RegExp("^[0-9]{4}$"),true],DI:[new RegExp("^6(011|4[4-9][0-9]|5[0-9]{2})[0-9]{12}$"),new RegExp("^[0-9]{3}$"),true],JCB:[new RegExp("^(3[0-9]{15}|(2131|1800)[0-9]{11})$"),new RegExp("^[0-9]{3,4}$"),true],OT:[false,new RegExp("^([0-9]{3}|[0-9]{4})?$"),false]});function popWin(d,e,b){var e=window.open(d,e,b);e.focus()}function setLocation(b){window.location.href=b}function setPLocation(d,b){if(b){window.opener.focus()}window.opener.location.href=d}function setLanguageCode(e,f){var b=window.location.href;var h="",g;if(g=b.match(/\#(.*)$/)){b=b.replace(/\#(.*)$/,"");h=g[0]}if(b.match(/[?]/)){var d=/([?&]store=)[a-z0-9_]*/;if(b.match(d)){b=b.replace(d,"$1"+e)}else{b+="&store="+e}var d=/([?&]from_store=)[a-z0-9_]*/;if(b.match(d)){b=b.replace(d,"")}}else{b+="?store="+e}if(typeof(f)!="undefined"){b+="&from_store="+f}b+=h;setLocation(b)}function decorateGeneric(h,e){var l=["odd","even","first","last"];var d={};var g=h.length;if(g){if(typeof(e)=="undefined"){e=l}if(!e.length){return}for(var b in l){d[l[b]]=false}for(var b in e){d[e[b]]=true}if(d.first){Element.addClassName(h[0],"first")}if(d.last){Element.addClassName(h[g-1],"last")}for(var f=0;f-1){b="?"+f.substring(d+2);f=f.substring(0,d+1)}return f+e+b}function formatCurrency(n,q,g){var l=isNaN(q.precision=Math.abs(q.precision))?2:q.precision;var v=isNaN(q.requiredPrecision=Math.abs(q.requiredPrecision))?2:q.requiredPrecision;l=v;var t=isNaN(q.integerRequired=Math.abs(q.integerRequired))?1:q.integerRequired;var p=q.decimalSymbol==undefined?",":q.decimalSymbol;var e=q.groupSymbol==undefined?".":q.groupSymbol;var d=q.groupLength==undefined?3:q.groupLength;var u="";if(g==undefined||g==true){u=n<0?"-":(g?"+":"")}else{if(g==false){u=""}}var h=parseInt(n=Math.abs(+n||0).toFixed(l))+"";var f=(h.lengthd?j%d:0;re=new RegExp("(\\d{"+d+"})(?=\\d)","g");var b=(j?h.substr(0,j)+e:"")+h.substr(j).replace(re,"$1"+e)+(l?p+Math.abs(n-h).toFixed(l).replace(/-/,0).slice(2):"");var o="";if(q.pattern.indexOf("{sign}")==-1){o=u+q.pattern}else{o=q.pattern.replace("{sign}",u)}return o.replace("%s",b).replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function expandDetails(d,b){if(Element.hasClassName(d,"show-details")){$$(b).each(function(e){e.hide()});Element.removeClassName(d,"show-details")}else{$$(b).each(function(e){e.show()});Element.addClassName(d,"show-details")}}var isIE=navigator.appVersion.match(/MSIE/)=="MSIE";if(!window.Varien){var Varien=new Object()}Varien.showLoading=function(){var b=$("loading-process");b&&b.show()};Varien.hideLoading=function(){var b=$("loading-process");b&&b.hide()};Varien.GlobalHandlers={onCreate:function(){Varien.showLoading()},onComplete:function(){if(Ajax.activeRequestCount==0){Varien.hideLoading()}}};Ajax.Responders.register(Varien.GlobalHandlers);Varien.searchForm=Class.create();Varien.searchForm.prototype={initialize:function(d,e,b){this.form=$(d);this.field=$(e);this.emptyText=b;Event.observe(this.form,"submit",this.submit.bind(this));Event.observe(this.field,"focus",this.focus.bind(this));Event.observe(this.field,"blur",this.blur.bind(this));this.blur()},submit:function(b){if(this.field.value==this.emptyText||this.field.value==""){Event.stop(b);return false}return true},focus:function(b){if(this.field.value==this.emptyText){this.field.value=""}},blur:function(b){if(this.field.value==""){this.field.value=this.emptyText}}};Varien.DateElement=Class.create();Varien.DateElement.prototype={initialize:function(b,d,f,e){if(b=="id"){this.day=$(d+"day");this.month=$(d+"month");this.year=$(d+"year");this.full=$(d+"full");this.advice=$(d+"date-advice")}else{if(b=="container"){this.day=d.day;this.month=d.month;this.year=d.year;this.full=d.full;this.advice=d.advice}else{return}}this.required=f;this.format=e;this.day.addClassName("validate-custom");this.day.validate=this.validate.bind(this);this.month.addClassName("validate-custom");this.month.validate=this.validate.bind(this);this.year.addClassName("validate-custom");this.year.validate=this.validate.bind(this);this.setDateRange(false,false);this.year.setAttribute("autocomplete","off");this.advice.hide()},validate:function(){var l=false,o=parseInt(this.day.value,10)||0,f=parseInt(this.month.value,10)||0,h=parseInt(this.year.value,10)||0;if(this.day.value.strip().empty()&&this.month.value.strip().empty()&&this.year.value.strip().empty()){if(this.required){l="Please enter a date."}else{this.full.value=""}}else{if(!o||!f||!h){l="Please enter a valid full date."}else{var d=new Date,n=0,e=null;d.setYear(h);d.setMonth(f-1);d.setDate(32);n=32-d.getDate();if(!n||n>31){n=31}if(o<1||o>n){e="day";l="Please enter a valid day (1-%1)."}else{if(f<1||f>12){e="month";l="Please enter a valid month (1-12)."}else{if(o%10==o){this.day.value="0"+o}if(f%10==f){this.month.value="0"+f}this.full.value=this.format.replace(/%[mb]/i,this.month.value).replace(/%[de]/i,this.day.value).replace(/%y/i,this.year.value);var b=this.month.value+"/"+this.day.value+"/"+this.year.value;var g=new Date(b);if(isNaN(g)){l="Please enter a valid date."}else{this.setFullDate(g)}}}var p=false;if(!l&&!this.validateData()){e=this.validateDataErrorType;p=this.validateDataErrorText;l=p}}}if(l!==false){if(jQuery.mage.__){l=jQuery.mage.__(l)}if(!p){this.advice.innerHTML=l.replace("%1",n)}else{this.advice.innerHTML=this.errorTextModifier(l)}this.advice.show();return false}this.day.removeClassName("validation-failed");this.month.removeClassName("validation-failed");this.year.removeClassName("validation-failed");this.advice.hide();return true},validateData:function(){var d=this.fullDate.getFullYear();var b=new Date;this.curyear=b.getFullYear();return(d>=1900&&d<=this.curyear)},validateDataErrorType:"year",validateDataErrorText:"Please enter a valid year (1900-%1).",errorTextModifier:function(b){return b.replace("%1",this.curyear)},setDateRange:function(b,d){this.minDate=b;this.maxDate=d},setFullDate:function(b){this.fullDate=b}};Varien.DOB=Class.create();Varien.DOB.prototype={initialize:function(b,g,f){var e=$$(b)[0];var d={};d.day=Element.select(e,".dob-day input")[0];d.month=Element.select(e,".dob-month input")[0];d.year=Element.select(e,".dob-year input")[0];d.full=Element.select(e,".dob-full input")[0];d.advice=Element.select(e,".validation-advice")[0];new Varien.DateElement("container",d,g,f)}};Varien.dateRangeDate=Class.create();Varien.dateRangeDate.prototype=Object.extend(new Varien.DateElement(),{validateData:function(){var b=true;if(this.minDate||this.maxValue){if(this.minDate){this.minDate=new Date(this.minDate);this.minDate.setHours(0);if(isNaN(this.minDate)){this.minDate=new Date("1/1/1900")}b=b&&(this.fullDate>=this.minDate)}if(this.maxDate){this.maxDate=new Date(this.maxDate);this.minDate.setHours(0);if(isNaN(this.maxDate)){this.maxDate=new Date()}b=b&&(this.fullDate<=this.maxDate)}if(this.maxDate&&this.minDate){this.validateDataErrorText="Please enter a valid date between %s and %s"}else{if(this.maxDate){this.validateDataErrorText="Please enter a valid date less than or equal to %s"}else{if(this.minDate){this.validateDataErrorText="Please enter a valid date equal to or greater than %s"}else{this.validateDataErrorText=""}}}}return b},validateDataErrorText:"Date should be between %s and %s",errorTextModifier:function(b){if(this.minDate){b=b.sub("%s",this.dateFormat(this.minDate))}if(this.maxDate){b=b.sub("%s",this.dateFormat(this.maxDate))}return b},dateFormat:function(b){return(b.getMonth()+1)+"/"+b.getDate()+"/"+b.getFullYear()}});Varien.FileElement=Class.create();Varien.FileElement.prototype={initialize:function(b){this.fileElement=$(b);this.hiddenElement=$(b+"_value");this.fileElement.observe("change",this.selectFile.bind(this))},selectFile:function(b){this.hiddenElement.value=this.fileElement.getValue()}};Validation.addAllThese([["validate-custom"," ",function(b,d){return d.validate()}]]);Element.addMethods({getInnerText:function(b){b=$(b);if(b.innerText&&!Prototype.Browser.Opera){return b.innerText}return b.innerHTML.stripScripts().unescapeHTML().replace(/[\n\r\s]+/g," ").strip()}});function fireEvent(d,e){if(document.createEvent){var b=document.createEvent("HTMLEvents");b.initEvent(e,true,true);return d.dispatchEvent(b)}else{var b=document.createEventObject();return d.fireEvent("on"+e,b)}}function modulo(b,f){var e=f/10000;var d=b%f;if(Math.abs(d-f)b.toFixed(2).toString().length){b=b.toFixed(2)}return b+" "+d[e]};var SessionError=Class.create();SessionError.prototype={initialize:function(b){this.errorText=b},toString:function(){return"Session Error:"+this.errorText}};Ajax.Request.addMethods({initialize:function($super,d,b){$super(b);this.transport=Ajax.getTransport();if(!d.match(new RegExp("[?&]isAjax=true",""))){d=d.match(new RegExp("\\?","g"))?d+"&isAjax=true":d+"?isAjax=true"}if(Object.isString(this.options.parameters)&&this.options.parameters.indexOf("form_key=")==-1){this.options.parameters+="&"+Object.toQueryString({form_key:FORM_KEY})}else{if(!this.options.parameters){this.options.parameters={form_key:FORM_KEY}}if(!this.options.parameters.form_key){this.options.parameters.form_key=FORM_KEY}}this.request(d)},respondToReadyState:function(b){var g=Ajax.Request.Events[b],d=new Ajax.Response(this);if(g=="Complete"){try{this._complete=true;if(d.responseText.isJSON()){var f=d.responseText.evalJSON();if(f.ajaxExpired&&f.ajaxRedirect){window.location.replace(f.ajaxRedirect);throw new SessionError("session expired")}}(this.options["on"+d.status]||this.options["on"+(this.success()?"Success":"Failure")]||Prototype.emptyFunction)(d,d.headerJSON)}catch(h){this.dispatchException(h);if(h instanceof SessionError){return}}var l=d.getHeader("Content-type");if(this.options.evalJS=="force"||(this.options.evalJS&&this.isSameOrigin()&&l&&l.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))){this.evalResponse()}}try{(this.options["on"+g]||Prototype.emptyFunction)(d,d.headerJSON);Ajax.Responders.dispatch("on"+g,this,d,d.headerJSON)}catch(h){this.dispatchException(h)}if(g=="Complete"){this.transport.onreadystatechange=Prototype.emptyFunction}}});Ajax.Updater.respondToReadyState=Ajax.Request.respondToReadyState;var varienLoader=new Class.create();varienLoader.prototype={initialize:function(b){this.callback=false;this.cache=$H();this.caching=b||false;this.url=false},getCache:function(b){if(this.cache.get(b)){return this.cache.get(b)}return false},load:function(b,d,f){this.url=b;this.callback=f;if(this.caching){var e=this.getCache(b);if(e){this.processResult(e);return}}if(typeof(d.updaterId)!="undefined"){new varienUpdater(d.updaterId,b,{evalScripts:true,onComplete:this.processResult.bind(this),onFailure:this._processFailure.bind(this)})}else{new Ajax.Request(b,{method:"post",parameters:d||{},onComplete:this.processResult.bind(this),onFailure:this._processFailure.bind(this)})}},_processFailure:function(b){location.href=BASE_URL},processResult:function(b){if(this.caching){this.cache.set(this.url,b)}if(this.callback){this.callback(b.responseText)}}};if(!window.varienLoaderHandler){var varienLoaderHandler=new Object()}varienLoaderHandler.handler={onCreate:function(b){if(b.options.loaderArea===false){return}jQuery("body").trigger("processStart")},onException:function(b){jQuery("body").trigger("processStop")},onComplete:function(b){jQuery("body").trigger("processStop")}};function setLoaderPosition(){var e=$("loading_mask_loader");if(e&&Prototype.Browser.IE){var d=e.getDimensions();var f=document.viewport.getDimensions();var b=document.viewport.getScrollOffsets();e.style.left=Math.floor(f.width/2+b.left-d.width/2)+"px";e.style.top=Math.floor(f.height/2+b.top-d.height/2)+"px";e.style.position="absolute"}}function toggleSelectsUnderBlock(f,b){if(Prototype.Browser.IE){var e=document.getElementsByTagName("select");for(var d=0;d');d.document.close();Event.observe(d,"load",function(){var e=d.document.getElementById("image_preview");d.resizeTo(e.width+40,e.height+80)})}}function checkByProductPriceType(b){if(b.id=="price_type"){this.productPriceType=b.value;return false}else{if(b.id=="price"&&this.productPriceType==0){return false}return true}}Event.observe(window,"load",function(){if($("price_default")&&$("price_default").checked){$("price").disabled="disabled"}});function toggleSeveralValueElements(f,e,b,d){if(e&&f){if(Object.prototype.toString.call(e)!="[object Array]"){e=[e]}e.each(function(g){toggleValueElements(f,g,b,d)})}}function toggleValueElements(l,d,f,h){if(d&&l){var n=[l];if(typeof f!="undefined"){if(Object.prototype.toString.call(f)!="[object Array]"){f=[f]}for(var g=0;g>2;l=((p&3)<<4)|(n>>4);g=((n&15)<<2)|(h>>6);f=h&63;if(isNaN(n)){g=f=64}else{if(isNaN(h)){f=64}}b=b+this._keyStr.charAt(o)+this._keyStr.charAt(l)+this._keyStr.charAt(g)+this._keyStr.charAt(f)}return b},decode:function(e){var b="";var p,n,h;var o,l,g,f;var d=0;if(typeof window.atob==="function"){return window.atob(e)}e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(d>4);n=((l&15)<<4)|(g>>2);h=((g&3)<<6)|f;b=b+String.fromCharCode(p);if(g!=64){b=b+String.fromCharCode(n)}if(f!=64){b=b+String.fromCharCode(h)}}b=Base64._utf8_decode(b);return b},mageEncode:function(b){return this.encode(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,",")},mageDecode:function(b){b=b.replace(/\-/g,"+").replace(/_/g,"/").replace(/,/g,"=");return this.decode(b)},idEncode:function(b){return this.encode(b).replace(/\+/g,":").replace(/\//g,"_").replace(/=/g,"-")},idDecode:function(b){b=b.replace(/\-/g,"=").replace(/_/g,"/").replace(/\:/g,"+");return this.decode(b)},_utf8_encode:function(d){d=d.replace(/\r\n/g,"\n");var b="";for(var f=0;f127)&&(e<2048)){b+=String.fromCharCode((e>>6)|192);b+=String.fromCharCode((e&63)|128)}else{b+=String.fromCharCode((e>>12)|224);b+=String.fromCharCode(((e>>6)&63)|128);b+=String.fromCharCode((e&63)|128)}}}return b},_utf8_decode:function(b){var d="";var e=0;var f=c1=c2=0;while(e191)&&(f<224)){c2=b.charCodeAt(e+1);d+=String.fromCharCode(((f&31)<<6)|(c2&63));e+=2}else{c2=b.charCodeAt(e+1);c3=b.charCodeAt(e+2);d+=String.fromCharCode(((f&15)<<12)|((c2&63)<<6)|(c3&63));e+=3}}}return d}};function sortNumeric(d,b){return d-b}(function(){var globals=["Prototype","Abstract","Try","Class","PeriodicalExecuter","Template","$break","Enumerable","$A","$w","$H","Hash","$R","ObjectRange","Ajax","$","Form","Field","$F","Toggle","Insertion","$continue","Position","Windows","Dialog","array","WindowUtilities","Builder","Effect","validateCreditCard","Validator","Validation","removeDelimiters","parseNumber","popWin","setLocation","setPLocation","setLanguageCode","decorateGeneric","decorateTable","decorateList","decorateDataList","parseSidUrl","formatCurrency","expandDetails","isIE","Varien","fireEvent","modulo","byteConvert","SessionError","varienLoader","varienLoaderHandler","setLoaderPosition","toggleSelectsUnderBlock","varienUpdater","setElementDisable","toggleParentVis","toggleFieldsetVis","toggleVis","imagePreview","checkByProductPriceType","toggleSeveralValueElements","toggleValueElements","submitAndReloadArea","syncOnchangeValue","updateElementAtCursor","firebugEnabled","disableElement","enableElement","disableElements","enableElements","Cookie","Fieldset","Base64","sortNumeric","Element","$$","Sizzle","Selector","Window"];globals.forEach(function(prop){window[prop]=eval(prop)})})(); \ No newline at end of file +(function(){var w=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,p=0,g=Object.prototype.toString,u=false,o=true;[0,0].sort(function(){o=false;return 0});var d=function(L,B,I,D){I=I||[];var e=B=B||document;if(B.nodeType!==1&&B.nodeType!==9){return[]}if(!L||typeof L!=="string"){return I}var J=[],K,G,P,O,H,A,z=true,E=v(B),N=L;while((w.exec(""),K=w.exec(N))!==null){N=K[3];J.push(K[1]);if(K[2]){A=K[3];break}}if(J.length>1&&q.exec(L)){if(J.length===2&&h.relative[J[0]]){G=l(J[0]+J[1],B)}else{G=h.relative[J[0]]?[B]:d(J.shift(),B);while(J.length){L=J.shift();if(h.relative[L]){L+=J.shift()}G=l(L,G)}}}else{if(!D&&J.length>1&&B.nodeType===9&&!E&&h.match.ID.test(J[0])&&!h.match.ID.test(J[J.length-1])){var Q=d.find(J.shift(),B,E);B=Q.expr?d.filter(Q.expr,Q.set)[0]:Q.set[0]}if(B){var Q=D?{expr:J.pop(),set:b(D)}:d.find(J.pop(),J.length===1&&(J[0]==="~"||J[0]==="+")&&B.parentNode?B.parentNode:B,E);G=Q.expr?d.filter(Q.expr,Q.set):Q.set;if(J.length>0){P=b(G)}else{z=false}while(J.length){var C=J.pop(),F=C;if(!h.relative[C]){C=""}else{F=J.pop()}if(F==null){F=B}h.relative[C](P,F,E)}}else{P=J=[]}}if(!P){P=G}if(!P){throw"Syntax error, unrecognized expression: "+(C||L)}if(g.call(P)==="[object Array]"){if(!z){I.push.apply(I,P)}else{if(B&&B.nodeType===1){for(var M=0;P[M]!=null;M++){if(P[M]&&(P[M]===true||P[M].nodeType===1&&n(B,P[M]))){I.push(G[M])}}}else{for(var M=0;P[M]!=null;M++){if(P[M]&&P[M].nodeType===1){I.push(G[M])}}}}}else{b(P,I)}if(A){d(A,e,I,D);d.uniqueSort(I)}return I};d.uniqueSort=function(z){if(f){u=o;z.sort(f);if(u){for(var e=1;e":function(E,z,F){var C=typeof z==="string";if(C&&!/\W/.test(z)){z=F?z:z.toUpperCase();for(var A=0,e=E.length;A=0)){if(!A){e.push(D)}}else{if(A){z[C]=false}}}}return false},ID:function(e){return e[1].replace(/\\/g,"")},TAG:function(z,e){for(var A=0;e[A]===false;A++){}return e[A]&&v(e[A])?z[1]:z[1].toUpperCase()},CHILD:function(e){if(e[1]=="nth"){var z=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(e[2]=="even"&&"2n"||e[2]=="odd"&&"2n+1"||!/\D/.test(e[2])&&"0n+"+e[2]||e[2]);e[2]=(z[1]+(z[2]||1))-0;e[3]=z[3]-0}e[0]=p++;return e},ATTR:function(C,z,A,e,D,E){var B=C[1].replace(/\\/g,"");if(!E&&h.attrMap[B]){C[1]=h.attrMap[B]}if(C[2]==="~="){C[4]=" "+C[4]+" "}return C},PSEUDO:function(C,z,A,e,D){if(C[1]==="not"){if((w.exec(C[3])||"").length>1||/^\w/.test(C[3])){C[3]=d(C[3],null,null,z)}else{var B=d.filter(C[3],z,A,true^D);if(!A){e.push.apply(e,B)}return false}}else{if(h.match.POS.test(C[0])||h.match.CHILD.test(C[0])){return true}}return C},POS:function(e){e.unshift(true);return e}},filters:{enabled:function(e){return e.disabled===false&&e.type!=="hidden"},disabled:function(e){return e.disabled===true},checked:function(e){return e.checked===true},selected:function(e){e.parentNode.selectedIndex;return e.selected===true},parent:function(e){return !!e.firstChild},empty:function(e){return !e.firstChild},has:function(A,z,e){return !!d(e[3],A).length},header:function(e){return/h\d/i.test(e.nodeName)},text:function(e){return"text"===e.type},radio:function(e){return"radio"===e.type},checkbox:function(e){return"checkbox"===e.type},file:function(e){return"file"===e.type},password:function(e){return"password"===e.type},submit:function(e){return"submit"===e.type},image:function(e){return"image"===e.type},reset:function(e){return"reset"===e.type},button:function(e){return"button"===e.type||e.nodeName.toUpperCase()==="BUTTON"},input:function(e){return/input|select|textarea|button/i.test(e.nodeName)}},setFilters:{first:function(z,e){return e===0},last:function(A,z,e,B){return z===B.length-1},even:function(z,e){return e%2===0},odd:function(z,e){return e%2===1},lt:function(A,z,e){return ze[3]-0},nth:function(A,z,e){return e[3]-0==z},eq:function(A,z,e){return e[3]-0==z}},filter:{PSEUDO:function(E,A,B,F){var z=A[1],C=h.filters[z];if(C){return C(E,B,A,F)}else{if(z==="contains"){return(E.textContent||E.innerText||"").indexOf(A[3])>=0}else{if(z==="not"){var D=A[3];for(var B=0,e=D.length;B=0)}}},ID:function(z,e){return z.nodeType===1&&z.getAttribute("id")===e},TAG:function(z,e){return(e==="*"&&z.nodeType===1)||z.nodeName===e},CLASS:function(z,e){return(" "+(z.className||z.getAttribute("class"))+" ").indexOf(e)>-1},ATTR:function(D,B){var A=B[1],e=h.attrHandle[A]?h.attrHandle[A](D):D[A]!=null?D[A]:D.getAttribute(A),E=e+"",C=B[2],z=B[4];return e==null?C==="!=":C==="="?E===z:C==="*="?E.indexOf(z)>=0:C==="~="?(" "+E+" ").indexOf(z)>=0:!z?E&&e!==false:C==="!="?E!=z:C==="^="?E.indexOf(z)===0:C==="$="?E.substr(E.length-z.length)===z:C==="|="?E===z||E.substr(0,z.length+1)===z+"-":false},POS:function(C,z,A,D){var e=z[2],B=h.setFilters[e];if(B){return B(C,A,z,D)}}}};var q=h.match.POS;for(var s in h.match){h.match[s]=new RegExp(h.match[s].source+/(?![^\[]*\])(?![^\(]*\))/.source);h.leftMatch[s]=new RegExp(/(^(?:.|\r|\n)*?)/.source+h.match[s].source)}var b=function(z,e){z=Array.prototype.slice.call(z,0);if(e){e.push.apply(e,z);return e}return z};try{Array.prototype.slice.call(document.documentElement.childNodes,0)}catch(r){b=function(C,B){var z=B||[];if(g.call(C)==="[object Array]"){Array.prototype.push.apply(z,C)}else{if(typeof C.length==="number"){for(var A=0,e=C.length;A";var e=document.documentElement;e.insertBefore(z,e.firstChild);if(!!document.getElementById(A)){h.find.ID=function(C,D,E){if(typeof D.getElementById!=="undefined"&&!E){var B=D.getElementById(C[1]);return B?B.id===C[1]||typeof B.getAttributeNode!=="undefined"&&B.getAttributeNode("id").nodeValue===C[1]?[B]:undefined:[]}};h.filter.ID=function(D,B){var C=typeof D.getAttributeNode!=="undefined"&&D.getAttributeNode("id");return D.nodeType===1&&C&&C.nodeValue===B}}e.removeChild(z);e=z=null})();(function(){var e=document.createElement("div");e.appendChild(document.createComment(""));if(e.getElementsByTagName("*").length>0){h.find.TAG=function(z,D){var C=D.getElementsByTagName(z[1]);if(z[1]==="*"){var B=[];for(var A=0;C[A];A++){if(C[A].nodeType===1){B.push(C[A])}}C=B}return C}}e.innerHTML="";if(e.firstChild&&typeof e.firstChild.getAttribute!=="undefined"&&e.firstChild.getAttribute("href")!=="#"){h.attrHandle.href=function(z){return z.getAttribute("href",2)}}e=null})();if(document.querySelectorAll){(function(){var e=d,A=document.createElement("div");A.innerHTML="

";if(A.querySelectorAll&&A.querySelectorAll(".TEST").length===0){return}d=function(E,D,B,C){D=D||document;if(!C&&D.nodeType===9&&!v(D)){try{return b(D.querySelectorAll(E),B)}catch(F){}}return e(E,D,B,C)};for(var z in e){d[z]=e[z]}A=null})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var e=document.createElement("div");e.innerHTML="
";if(e.getElementsByClassName("e").length===0){return}e.lastChild.className="e";if(e.getElementsByClassName("e").length===1){return}h.order.splice(1,0,"CLASS");h.find.CLASS=function(z,A,B){if(typeof A.getElementsByClassName!=="undefined"&&!B){return A.getElementsByClassName(z[1])}};e=null})()}function t(z,E,D,I,F,H){var G=z=="previousSibling"&&!H;for(var B=0,A=I.length;B0){C=e;break}}}e=e[z]}I[B]=C}}}var n=document.compareDocumentPosition?function(z,e){return z.compareDocumentPosition(e)&16}:function(z,e){return z!==e&&(z.contains?z.contains(e):true)};var v=function(e){return e.nodeType===9&&e.documentElement.nodeName!=="HTML"||!!e.ownerDocument&&e.ownerDocument.documentElement.nodeName!=="HTML"};var l=function(e,F){var B=[],C="",D,A=F.nodeType?[F]:F;while((D=h.match.PSEUDO.exec(e))){C+=D[0];e=e.replace(h.match.PSEUDO,"")}e=h.relative[e]?e+"*":e;for(var E=0,z=A.length;E=0}).sortBy(function(f){return f.tabIndex}).first();return b?b:e.find(function(f){return/^(?:input|select|textarea)$/i.test(f.tagName)})},focusFirstElement:function(d){d=$(d);var b=d.findFirstElement();if(b){b.activate()}return d},request:function(d,b){d=$(d),b=Object.clone(b||{});var f=b.parameters,e=d.readAttribute("action")||"";if(e.blank()){e=window.location.href}b.parameters=d.serialize(true);if(f){if(Object.isString(f)){f=f.toQueryParams()}Object.extend(b.parameters,f)}if(d.hasAttribute("method")&&!b.method){b.method=d.method}return new Ajax.Request(e,b)}};Form.Element={focus:function(b){$(b).focus();return b},select:function(b){$(b).select();return b}};Form.Element.Methods={serialize:function(b){b=$(b);if(!b.disabled&&b.name){var d=b.getValue();if(d!=undefined){var e={};e[b.name]=d;return Object.toQueryString(e)}}return""},getValue:function(b){b=$(b);var d=b.tagName.toLowerCase();return Form.Element.Serializers[d](b)},setValue:function(b,d){b=$(b);var e=b.tagName.toLowerCase();Form.Element.Serializers[e](b,d);return b},clear:function(b){$(b).value="";return b},present:function(b){return $(b).value!=""},activate:function(b){b=$(b);try{b.focus();if(b.select&&(b.tagName.toLowerCase()!="input"||!(/^(?:button|reset|submit)$/i.test(b.type)))){b.select()}}catch(d){}return b},disable:function(b){b=$(b);b.disabled=true;return b},enable:function(b){b=$(b);b.disabled=false;return b}};var Field=Form.Element;var $F=Form.Element.Methods.getValue;Form.Element.Serializers=(function(){function d(n,o){switch(n.type.toLowerCase()){case"checkbox":case"radio":return h(n,o);default:return g(n,o)}}function h(n,o){if(Object.isUndefined(o)){return n.checked?n.value:null}else{n.checked=!!o}}function g(n,o){if(Object.isUndefined(o)){return n.value}else{n.value=o}}function b(p,s){if(Object.isUndefined(s)){return(p.type==="select-one"?e:f)(p)}var o,q,t=!Object.isArray(s);for(var n=0,r=p.length;n=0?l(o.options[n]):null}function f(q){var n,r=q.length;if(!r){return null}for(var p=0,n=[];p=this.offset[1]&&e=this.offset[0]&&b=this.offset[1]&&this.ycomp=this.offset[0]&&this.xcomp0})._each(b)},set:function(b){this.element.className=b},add:function(b){if(this.include(b)){return}this.set($A(this).concat(b).join(" "))},remove:function(b){if(!this.include(b)){return}this.set($A(this).without(b).join(" "))},toString:function(){return $A(this).join(" ")}};Object.extend(Element.ClassNames.prototype,Enumerable);(function(){window.Selector=Class.create({initialize:function(b){this.expression=b.strip()},findElements:function(b){return Prototype.Selector.select(this.expression,b)},match:function(b){return Prototype.Selector.match(b,this.expression)},toString:function(){return this.expression},inspect:function(){return"#"}});Object.extend(Selector,{matchElements:function(h,l){var b=Prototype.Selector.match,f=[];for(var e=0,g=h.length;e0){if(typeof arguments[0]=="string"){e=arguments[0];d=1}else{e=arguments[0]?arguments[0].id:null}}if(!e){e="window_"+new Date().getTime()}if($(e)){alert("Window "+e+" is already registered in the DOM! Make sure you use setDestroyOnClose() or destroyOnClose: true in the constructor")}this.options=Object.extend({className:"dialog",windowClassName:null,blurClassName:null,minWidth:100,minHeight:20,resizable:true,closable:true,minimizable:true,maximizable:true,draggable:true,userData:null,showEffect:(Window.hasEffectLib?Effect.Appear:Element.show),hideEffect:(Window.hasEffectLib?Effect.Fade:Element.hide),showEffectOptions:{},hideEffectOptions:{},effectOptions:null,parent:document.body,title:" ",url:null,onload:Prototype.emptyFunction,width:200,height:300,opacity:1,recenterAuto:true,wiredDrag:false,closeOnEsc:true,closeCallback:null,destroyOnClose:false,gridX:1,gridY:1},arguments[d]||{});if(this.options.blurClassName){this.options.focusClassName=this.options.className}if(typeof this.options.top=="undefined"&&typeof this.options.bottom=="undefined"){this.options.top=this._round(Math.random()*500,this.options.gridY)}if(typeof this.options.left=="undefined"&&typeof this.options.right=="undefined"){this.options.left=this._round(Math.random()*500,this.options.gridX)}if(this.options.effectOptions){Object.extend(this.options.hideEffectOptions,this.options.effectOptions);Object.extend(this.options.showEffectOptions,this.options.effectOptions);if(this.options.showEffect==Element.Appear){this.options.showEffectOptions.to=this.options.opacity}}if(Window.hasEffectLib){if(this.options.showEffect==Effect.Appear){this.options.showEffectOptions.to=this.options.opacity}if(this.options.hideEffect==Effect.Fade){this.options.hideEffectOptions.from=this.options.opacity}}if(this.options.hideEffect==Element.hide){this.options.hideEffect=function(){Element.hide(this.element);if(this.options.destroyOnClose){this.destroy()}}.bind(this)}if(this.options.parent!=document.body){this.options.parent=$(this.options.parent)}this.element=this._createWindow(e);this.element.win=this;this.eventMouseDown=this._initDrag.bindAsEventListener(this);this.eventMouseUp=this._endDrag.bindAsEventListener(this);this.eventMouseMove=this._updateDrag.bindAsEventListener(this);this.eventOnLoad=this._getWindowBorderSize.bindAsEventListener(this);this.eventMouseDownContent=this.toFront.bindAsEventListener(this);this.eventResize=this._recenter.bindAsEventListener(this);this.eventKeyUp=this._keyUp.bindAsEventListener(this);this.topbar=$(this.element.id+"_top");this.bottombar=$(this.element.id+"_bottom");this.content=$(this.element.id+"_content");Event.observe(this.topbar,"mousedown",this.eventMouseDown);Event.observe(this.bottombar,"mousedown",this.eventMouseDown);Event.observe(this.content,"mousedown",this.eventMouseDownContent);Event.observe(window,"load",this.eventOnLoad);Event.observe(window,"resize",this.eventResize);Event.observe(window,"scroll",this.eventResize);Event.observe(document,"keyup",this.eventKeyUp);Event.observe(this.options.parent,"scroll",this.eventResize);if(this.options.draggable){var b=this;[this.topbar,this.topbar.up().previous(),this.topbar.up().next()].each(function(f){f.observe("mousedown",b.eventMouseDown);f.addClassName("top_draggable")});[this.bottombar.up(),this.bottombar.up().previous(),this.bottombar.up().next()].each(function(f){f.observe("mousedown",b.eventMouseDown);f.addClassName("bottom_draggable")})}if(this.options.resizable){this.sizer=$(this.element.id+"_sizer");Event.observe(this.sizer,"mousedown",this.eventMouseDown)}this.useLeft=null;this.useTop=null;if(typeof this.options.left!="undefined"){this.element.setStyle({left:parseFloat(this.options.left)+"px"});this.useLeft=true}else{this.element.setStyle({right:parseFloat(this.options.right)+"px"});this.useLeft=false}if(typeof this.options.top!="undefined"){this.element.setStyle({top:parseFloat(this.options.top)+"px"});this.useTop=true}else{this.element.setStyle({bottom:parseFloat(this.options.bottom)+"px"});this.useTop=false}this.storedLocation=null;this.setOpacity(this.options.opacity);if(this.options.zIndex){this.setZIndex(this.options.zIndex)}else{this.setZIndex(this.getMaxZIndex())}if(this.options.destroyOnClose){this.setDestroyOnClose(true)}this._getWindowBorderSize();this.width=this.options.width;this.height=this.options.height;this.visible=false;this.constraint=false;this.constraintPad={top:0,left:0,bottom:0,right:0};if(this.width&&this.height){this.setSize(this.options.width,this.options.height)}this.setTitle(this.options.title);Windows.register(this)},getMaxZIndex:function(){var b=0,d;var g=document.body.childNodes;for(d=0;d ';$(this.getId()+"_table_content").innerHTML=d;this.content=$(this.element.id+"_content")}this.getContent().innerHTML=b},setAjaxContent:function(d,b,f,e){this.showFunction=f?"showCenter":"show";this.showModal=e||false;b=b||{};this.setHTMLContent("");this.onComplete=b.onComplete;if(!this._onCompleteHandler){this._onCompleteHandler=this._setAjaxContent.bind(this)}b.onComplete=this._onCompleteHandler;new Ajax.Request(d,b);b.onComplete=this.onComplete},_setAjaxContent:function(b){Element.update(this.getContent(),b.responseText);if(this.onComplete){this.onComplete(b)}this.onComplete=null;this[this.showFunction](this.showModal)},setURL:function(b){if(this.options.url){this.content.src=null}this.options.url=b;var d="";$(this.getId()+"_table_content").innerHTML=d;this.content=$(this.element.id+"_content")},getURL:function(){return this.options.url?this.options.url:null},refresh:function(){if(this.options.url){$(this.element.getAttribute("id")+"_content").src=this.options.url}},setCookie:function(d,e,t,g,b){d=d||this.element.id;this.cookie=[d,e,t,g,b];var r=WindowUtilities.getCookie(d);if(r){var s=r.split(",");var p=s[0].split(":");var o=s[1].split(":");var q=parseFloat(s[2]),l=parseFloat(s[3]);var n=s[4];var f=s[5];this.setSize(q,l);if(n=="true"){this.doMinimize=true}else{if(f=="true"){this.doMaximize=true}}this.useLeft=p[0]=="l";this.useTop=o[0]=="t";this.element.setStyle(this.useLeft?{left:p[1]}:{right:p[1]});this.element.setStyle(this.useTop?{top:o[1]}:{bottom:o[1]})}},getId:function(){return this.element.id},setDestroyOnClose:function(){this.options.destroyOnClose=true},setConstraint:function(b,d){this.constraint=b;this.constraintPad=Object.extend(this.constraintPad,d||{});if(this.useTop&&this.useLeft){this.setLocation(parseFloat(this.element.style.top),parseFloat(this.element.style.left))}},_initDrag:function(d){if(Event.element(d)==this.sizer&&this.isMinimized()){return}if(Event.element(d)!=this.sizer&&this.isMaximized()){return}if(Prototype.Browser.IE&&this.heightN==0){this._getWindowBorderSize()}this.pointer=[this._round(Event.pointerX(d),this.options.gridX),this._round(Event.pointerY(d),this.options.gridY)];if(this.options.wiredDrag){this.currentDrag=this._createWiredElement()}else{this.currentDrag=this.element}if(Event.element(d)==this.sizer){this.doResize=true;this.widthOrg=this.width;this.heightOrg=this.height;this.bottomOrg=parseFloat(this.element.getStyle("bottom"));this.rightOrg=parseFloat(this.element.getStyle("right"));this._notify("onStartResize")}else{this.doResize=false;var b=$(this.getId()+"_close");if(b&&Position.within(b,this.pointer[0],this.pointer[1])){this.currentDrag=null;return}this.toFront();if(!this.options.draggable){return}this._notify("onStartMove")}Event.observe(document,"mouseup",this.eventMouseUp,false);Event.observe(document,"mousemove",this.eventMouseMove,false);WindowUtilities.disableScreen("__invisible__","__invisible__",this.overlayOpacity);document.body.ondrag=function(){return false};document.body.onselectstart=function(){return false};this.currentDrag.show();Event.stop(d)},_round:function(d,b){return b==1?d:d=Math.floor(d/b)*b},_updateDrag:function(d){var b=[this._round(Event.pointerX(d),this.options.gridX),this._round(Event.pointerY(d),this.options.gridY)];var q=b[0]-this.pointer[0];var p=b[1]-this.pointer[1];if(this.doResize){var o=this.widthOrg+q;var f=this.heightOrg+p;q=this.width-this.widthOrg;p=this.height-this.heightOrg;if(this.useLeft){o=this._updateWidthConstraint(o)}else{this.currentDrag.setStyle({right:(this.rightOrg-q)+"px"})}if(this.useTop){f=this._updateHeightConstraint(f)}else{this.currentDrag.setStyle({bottom:(this.bottomOrg-p)+"px"})}this.setSize(o,f);this._notify("onResize")}else{this.pointer=b;if(this.useLeft){var e=parseFloat(this.currentDrag.getStyle("left"))+q;var n=this._updateLeftConstraint(e);this.pointer[0]+=n-e;this.currentDrag.setStyle({left:n+"px"})}else{this.currentDrag.setStyle({right:parseFloat(this.currentDrag.getStyle("right"))-q+"px"})}if(this.useTop){var l=parseFloat(this.currentDrag.getStyle("top"))+p;var g=this._updateTopConstraint(l);this.pointer[1]+=g-l;this.currentDrag.setStyle({top:g+"px"})}else{this.currentDrag.setStyle({bottom:parseFloat(this.currentDrag.getStyle("bottom"))-p+"px"})}this._notify("onMove")}if(this.iefix){this._fixIEOverlapping()}this._removeStoreLocation();Event.stop(d)},_endDrag:function(b){WindowUtilities.enableScreen("__invisible__");if(this.doResize){this._notify("onEndResize")}else{this._notify("onEndMove")}Event.stopObserving(document,"mouseup",this.eventMouseUp,false);Event.stopObserving(document,"mousemove",this.eventMouseMove,false);Event.stop(b);this._hideWiredElement();this._saveCookie();document.body.ondrag=null;document.body.onselectstart=null},_updateLeftConstraint:function(d){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowWidth:this.options.parent.getDimensions().width;if(db-this.constraintPad.right){d=b-this.constraintPad.right-this.width-this.widthE-this.widthW}}return d},_updateTopConstraint:function(e){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowHeight:this.options.parent.getDimensions().height;var d=this.height+this.heightN+this.heightS;if(eb-this.constraintPad.bottom){e=b-this.constraintPad.bottom-d}}return e},_updateWidthConstraint:function(b){if(this.constraint&&this.useLeft&&this.useTop){var d=this.options.parent==document.body?WindowUtilities.getPageSize().windowWidth:this.options.parent.getDimensions().width;var e=parseFloat(this.element.getStyle("left"));if(e+b+this.widthE+this.widthW>d-this.constraintPad.right){b=d-this.constraintPad.right-e-this.widthE-this.widthW}}return b},_updateHeightConstraint:function(d){if(this.constraint&&this.useLeft&&this.useTop){var b=this.options.parent==document.body?WindowUtilities.getPageSize().windowHeight:this.options.parent.getDimensions().height;var e=parseFloat(this.element.getStyle("top"));if(e+d+this.heightN+this.heightS>b-this.constraintPad.bottom){d=b-this.constraintPad.bottom-e-this.heightN-this.heightS}}return d},_createWindow:function(b){var h=this.options.className;var f=document.createElement("div");f.setAttribute("id",b);f.className="dialog";if(this.options.windowClassName){f.className+=" "+this.options.windowClassName}var g;if(this.options.url){g=''}else{g='
'}var l=this.options.closable?"
":"";var n=this.options.minimizable?"
":"";var o=this.options.maximizable?"
":"";var e=this.options.resizable?"class='"+h+"_sizer' id='"+b+"_sizer'":"class='"+h+"_se'";var d="../themes/default/blank.gif";f.innerHTML=l+n+o+"
"+this.options.title+"
"+g+"
";Element.hide(f);this.options.parent.insertBefore(f,this.options.parent.firstChild);Event.observe($(b+"_content"),"load",this.options.onload);return f},changeClassName:function(b){var d=this.options.className;var e=this.getId();$A(["_close","_minimize","_maximize","_sizer","_content"]).each(function(f){this._toggleClassName($(e+f),d+f,b+f)}.bind(this));this._toggleClassName($(e+"_top"),d+"_title",b+"_title");$$("#"+e+" td").each(function(f){f.className=f.className.sub(d,b)});this.options.className=b},_toggleClassName:function(e,d,b){if(e){e.removeClassName(d);e.addClassName(b)}},setLocation:function(f,d){f=this._updateTopConstraint(f);d=this._updateLeftConstraint(d);var b=this.currentDrag||this.element;b.setStyle({top:f+"px"});b.setStyle({left:d+"px"});this.useLeft=true;this.useTop=true},getLocation:function(){var b={};if(this.useTop){b=Object.extend(b,{top:this.element.getStyle("top")})}else{b=Object.extend(b,{bottom:this.element.getStyle("bottom")})}if(this.useLeft){b=Object.extend(b,{left:this.element.getStyle("left")})}else{b=Object.extend(b,{right:this.element.getStyle("right")})}return b},getSize:function(){return{width:this.width,height:this.height}},setSize:function(f,d,b){f=parseFloat(f);d=parseFloat(d);if(!this.minimized&&fthis.options.maxHeight){d=this.options.maxHeight}if(this.options.maxWidth&&f>this.options.maxWidth){f=this.options.maxWidth}if(this.useTop&&this.useLeft&&Window.hasEffectLib&&Effect.ResizeWindow&&b){new Effect.ResizeWindow(this,null,null,f,d,{duration:Window.resizeEffectDuration})}else{this.width=f;this.height=d;var h=this.currentDrag?this.currentDrag:this.element;h.setStyle({width:f+this.widthW+this.widthE+"px"});h.setStyle({height:d+this.heightN+this.heightS+"px"});if(!this.currentDrag||this.currentDrag==this.element){var g=$(this.element.id+"_content");g.setStyle({height:d+"px"});g.setStyle({width:f+"px"})}}},updateHeight:function(){this.setSize(this.width,this.content.scrollHeight,true)},updateWidth:function(){this.setSize(this.content.scrollWidth,this.height,true)},toFront:function(){if(this.element.style.zIndex0)&&(navigator.userAgent.indexOf("Opera")<0)&&(this.element.getStyle("position")=="absolute")){new Insertion.After(this.element.id,'');this.iefix=$(this.element.id+"_iefix")}if(this.iefix){setTimeout(this._fixIEOverlapping.bind(this),50)}},_fixIEOverlapping:function(){Position.clone(this.element,this.iefix);this.iefix.style.zIndex=this.element.style.zIndex-1;this.iefix.show()},_keyUp:function(b){if(27==b.keyCode&&this.options.closeOnEsc){this.close()}},_getWindowBorderSize:function(d){var e=this._createHiddenDiv(this.options.className+"_n");this.heightN=Element.getDimensions(e).height;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_s");this.heightS=Element.getDimensions(e).height;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_e");this.widthE=Element.getDimensions(e).width;e.parentNode.removeChild(e);var e=this._createHiddenDiv(this.options.className+"_w");this.widthW=Element.getDimensions(e).width;e.parentNode.removeChild(e);var e=document.createElement("div");e.className="overlay_"+this.options.className;document.body.appendChild(e);var b=this;setTimeout(function(){b.overlayOpacity=($(e).getStyle("opacity"));e.parentNode.removeChild(e)},10);if(Prototype.Browser.IE){this.heightS=$(this.getId()+"_row3").getDimensions().height;this.heightN=$(this.getId()+"_row1").getDimensions().height}if(Prototype.Browser.WebKit&&Prototype.Browser.WebKitVersion<420){this.setSize(this.width,this.height)}if(this.doMaximize){this.maximize()}if(this.doMinimize){this.minimize()}},_createHiddenDiv:function(d){var b=document.body;var e=document.createElement("div");e.setAttribute("id",this.element.id+"_tmp");e.className=d;e.style.display="none";e.innerHTML="";b.insertBefore(e,b.firstChild);return e},_storeLocation:function(){if(this.storedLocation==null){this.storedLocation={useTop:this.useTop,useLeft:this.useLeft,top:this.element.getStyle("top"),bottom:this.element.getStyle("bottom"),left:this.element.getStyle("left"),right:this.element.getStyle("right"),width:this.width,height:this.height}}},_restoreLocation:function(){if(this.storedLocation!=null){this.useLeft=this.storedLocation.useLeft;this.useTop=this.storedLocation.useTop;if(this.useLeft&&this.useTop&&Window.hasEffectLib&&Effect.ResizeWindow){new Effect.ResizeWindow(this,this.storedLocation.top,this.storedLocation.left,this.storedLocation.width,this.storedLocation.height,{duration:Window.resizeEffectDuration})}else{this.element.setStyle(this.useLeft?{left:this.storedLocation.left}:{right:this.storedLocation.right});this.element.setStyle(this.useTop?{top:this.storedLocation.top}:{bottom:this.storedLocation.bottom});this.setSize(this.storedLocation.width,this.storedLocation.height)}Windows.resetOverflow();this._removeStoreLocation()}},_removeStoreLocation:function(){this.storedLocation=null},_saveCookie:function(){if(this.cookie){var b="";if(this.useLeft){b+="l:"+(this.storedLocation?this.storedLocation.left:this.element.getStyle("left"))}else{b+="r:"+(this.storedLocation?this.storedLocation.right:this.element.getStyle("right"))}if(this.useTop){b+=",t:"+(this.storedLocation?this.storedLocation.top:this.element.getStyle("top"))}else{b+=",b:"+(this.storedLocation?this.storedLocation.bottom:this.element.getStyle("bottom"))}b+=","+(this.storedLocation?this.storedLocation.width:this.width);b+=","+(this.storedLocation?this.storedLocation.height:this.height);b+=","+this.isMinimized();b+=","+this.isMaximized();WindowUtilities.setCookie(b,this.cookie)}},_createWiredElement:function(){if(!this.wiredElement){if(Prototype.Browser.IE){this._getWindowBorderSize()}var d=document.createElement("div");d.className="wired_frame "+this.options.className+"_wired_frame";d.style.position="absolute";this.options.parent.insertBefore(d,this.options.parent.firstChild);this.wiredElement=$(d)}if(this.useLeft){this.wiredElement.setStyle({left:this.element.getStyle("left")})}else{this.wiredElement.setStyle({right:this.element.getStyle("right")})}if(this.useTop){this.wiredElement.setStyle({top:this.element.getStyle("top")})}else{this.wiredElement.setStyle({bottom:this.element.getStyle("bottom")})}var b=this.element.getDimensions();this.wiredElement.setStyle({width:b.width+"px",height:b.height+"px"});this.wiredElement.setStyle({zIndex:Windows.maxZIndex+30});return this.wiredElement},_hideWiredElement:function(){if(!this.wiredElement||!this.currentDrag){return}if(this.currentDrag==this.element){this.currentDrag=null}else{if(this.useLeft){this.element.setStyle({left:this.currentDrag.getStyle("left")})}else{this.element.setStyle({right:this.currentDrag.getStyle("right")})}if(this.useTop){this.element.setStyle({top:this.currentDrag.getStyle("top")})}else{this.element.setStyle({bottom:this.currentDrag.getStyle("bottom")})}this.currentDrag.hide();this.currentDrag=null;if(this.doResize){this.setSize(this.width,this.height)}}},_notify:function(b){if(this.options[b]){this.options[b](this)}else{Windows.notify(b,this)}}};var Windows={windows:[],modalWindows:[],observers:[],focusedWindow:null,maxZIndex:0,overlayShowEffectOptions:{duration:0.5},overlayHideEffectOptions:{duration:0.5},addObserver:function(b){this.removeObserver(b);this.observers.push(b)},removeObserver:function(b){this.observers=this.observers.reject(function(d){return d==b})},notify:function(b,d){this.observers.each(function(e){if(e[b]){e[b](b,d)}})},getWindow:function(b){return this.windows.detect(function(e){return e.getId()==b})},getFocusedWindow:function(){return this.focusedWindow},updateFocusedWindow:function(){this.focusedWindow=this.windows.length>=2?this.windows[this.windows.length-2]:null},register:function(b){this.windows.push(b)},addModalWindow:function(b){if(this.modalWindows.length==0){WindowUtilities.disableScreen(b.options.className,"overlay_modal",b.overlayOpacity,b.getId(),b.options.parent)}else{if(Window.keepMultiModalWindow){$("overlay_modal").style.zIndex=Windows.maxZIndex+1;Windows.maxZIndex+=1;WindowUtilities._hideSelect(this.modalWindows.last().getId())}else{this.modalWindows.last().element.hide()}WindowUtilities._showSelect(b.getId())}this.modalWindows.push(b)},removeModalWindow:function(b){this.modalWindows.pop();if(this.modalWindows.length==0){WindowUtilities.enableScreen()}else{if(Window.keepMultiModalWindow){this.modalWindows.last().toFront();WindowUtilities._showSelect(this.modalWindows.last().getId())}else{this.modalWindows.last().element.show()}}},register:function(b){this.windows.push(b)},unregister:function(b){this.windows=this.windows.reject(function(e){return e==b})},closeAll:function(){this.windows.each(function(b){Windows.close(b.getId())})},closeAllModalWindows:function(){WindowUtilities.enableScreen();this.modalWindows.each(function(b){if(b){b.close()}})},minimize:function(e,b){var d=this.getWindow(e);if(d&&d.visible){d.minimize()}Event.stop(b)},maximize:function(e,b){var d=this.getWindow(e);if(d&&d.visible){d.maximize()}Event.stop(b)},close:function(e,b){var d=this.getWindow(e);if(d){d.close()}if(b){Event.stop(b)}},blur:function(d){var b=this.getWindow(d);if(!b){return}if(b.options.blurClassName){b.changeClassName(b.options.blurClassName)}if(this.focusedWindow==b){this.focusedWindow=null}b._notify("onBlur")},focus:function(d){var b=this.getWindow(d);if(!b){return}if(this.focusedWindow){this.blur(this.focusedWindow.getId())}if(b.options.focusClassName){b.changeClassName(b.options.focusClassName)}this.focusedWindow=b;b._notify("onFocus")},unsetOverflow:function(b){this.windows.each(function(e){e.oldOverflow=e.getContent().getStyle("overflow")||"auto";e.getContent().setStyle({overflow:"hidden"})});if(b&&b.oldOverflow){b.getContent().setStyle({overflow:b.oldOverflow})}},resetOverflow:function(){this.windows.each(function(b){if(b.oldOverflow){b.getContent().setStyle({overflow:b.oldOverflow})}})},updateZindex:function(b,d){if(b>this.maxZIndex){this.maxZIndex=b;if(this.focusedWindow){this.blur(this.focusedWindow.getId())}}this.focusedWindow=d;if(this.focusedWindow){this.focus(this.focusedWindow.getId())}}};var Dialog={dialogId:null,onCompleteFunc:null,callFunc:null,parameters:null,confirm:function(f,e){if(f&&typeof f!="string"){Dialog._runAjaxRequest(f,e,Dialog.confirm);return}f=f||"";e=e||{};var h=e.okLabel?e.okLabel:"Ok";var b=e.cancelLabel?e.cancelLabel:"Cancel";e=Object.extend(e,e.windowParameters||{});e.windowParameters=e.windowParameters||{};e.className=e.className||"alert";var d="class ='"+(e.buttonClass?e.buttonClass+" ":"")+" ok_button'";var g="class ='"+(e.buttonClass?e.buttonClass+" ":"")+" cancel_button'";var f="
"+f+"
";return this._openDialog(f,e)},alert:function(e,d){if(e&&typeof e!="string"){Dialog._runAjaxRequest(e,d,Dialog.alert);return}e=e||"";d=d||{};var f=d.okLabel?d.okLabel:"Ok";d=Object.extend(d,d.windowParameters||{});d.windowParameters=d.windowParameters||{};d.className=d.className||"alert";var b="class ='"+(d.buttonClass?d.buttonClass+" ":"")+" ok_button'";var e="
"+e+"
";return this._openDialog(e,d)},info:function(d,b){if(d&&typeof d!="string"){Dialog._runAjaxRequest(d,b,Dialog.info);return}d=d||"";b=b||{};b=Object.extend(b,b.windowParameters||{});b.windowParameters=b.windowParameters||{};b.className=b.className||"alert";var d="";if(b.showProgress){d+=""}b.ok=null;b.cancel=null;return this._openDialog(d,b)},setInfoMessage:function(b){$("modal_dialog_message").update(b)},closeInfo:function(){Windows.close(this.dialogId)},_openDialog:function(g,f){var e=f.className;if(!f.height&&!f.width){f.width=WindowUtilities.getPageSize(f.options.parent||document.body).pageWidth/2}if(f.id){this.dialogId=f.id}else{var d=new Date();this.dialogId="modal_dialog_"+d.getTime();f.id=this.dialogId}if(!f.height||!f.width){var b=WindowUtilities._computeSize(g,this.dialogId,f.width,f.height,5,e);if(f.height){f.width=b+5}else{f.height=b+5}}f.effectOptions=f.effectOptions;f.resizable=f.resizable||false;f.minimizable=f.minimizable||false;f.maximizable=f.maximizable||false;f.draggable=f.draggable||false;f.closable=f.closable||false;var h=new Window(f);h.getContent().innerHTML=g;h.showCenter(true,f.top,f.left);h.setDestroyOnClose();h.cancelCallback=f.onCancel||f.cancel;h.okCallback=f.onOk||f.ok;return h},_getAjaxContent:function(b){Dialog.callFunc(b.responseText,Dialog.parameters)},_runAjaxRequest:function(e,d,b){if(e.options==null){e.options={}}Dialog.onCompleteFunc=e.options.onComplete;Dialog.parameters=d;Dialog.callFunc=b;e.options.onComplete=Dialog._getAjaxContent;new Ajax.Request(e.url,e.options)},okCallback:function(){var b=Windows.focusedWindow;if(!b.okCallback||b.okCallback(b)){$$("#"+b.getId()+" input").each(function(d){d.onclick=null});b.close()}},cancelCallback:function(){var b=Windows.focusedWindow;$$("#"+b.getId()+" input").each(function(d){d.onclick=null});b.close();if(b.cancelCallback){b.cancelCallback(b)}}};if(Prototype.Browser.WebKit){var array=navigator.userAgent.match(new RegExp(/AppleWebKit\/([\d\.\+]*)/));Prototype.Browser.WebKitVersion=parseFloat(array[1])}var WindowUtilities={getWindowScroll:function(parent){var T,L,W,H;parent=parent||document.body;if(parent!=document.body){T=parent.scrollTop;L=parent.scrollLeft;W=parent.scrollWidth;H=parent.scrollHeight}else{var w=window;with(w.document){if(w.document.documentElement&&documentElement.scrollTop){T=documentElement.scrollTop;L=documentElement.scrollLeft}else{if(w.document.body){T=body.scrollTop;L=body.scrollLeft}}if(w.innerWidth){W=w.innerWidth;H=w.innerHeight}else{if(w.document.documentElement&&documentElement.clientWidth){W=documentElement.clientWidth;H=documentElement.clientHeight}else{W=body.offsetWidth;H=body.offsetHeight}}}}return{top:T,left:L,width:W,height:H}},getPageSize:function(f){f=f||document.body;var e,l;var g,d;if(f!=document.body){e=f.getWidth();l=f.getHeight();d=f.scrollWidth;g=f.scrollHeight}else{var h,b;if(window.innerHeight&&window.scrollMaxY){h=document.body.scrollWidth;b=window.innerHeight+window.scrollMaxY}else{if(document.body.scrollHeight>document.body.offsetHeight){h=document.body.scrollWidth;b=document.body.scrollHeight}else{h=document.body.offsetWidth;b=document.body.offsetHeight}}if(self.innerHeight){e=document.documentElement.clientWidth;l=self.innerHeight}else{if(document.documentElement&&document.documentElement.clientHeight){e=document.documentElement.clientWidth;l=document.documentElement.clientHeight}else{if(document.body){e=document.body.clientWidth;l=document.body.clientHeight}}}if(b"}catch(h){}var g=d.firstChild||null;if(g&&(g.tagName.toUpperCase()!=b)){g=g.getElementsByTagName(b)[0]}if(!g){g=document.createElement(b)}if(!g){return}if(arguments[1]){if(this._isStringOrNumber(arguments[1])||(arguments[1] instanceof Array)||arguments[1].tagName){this._children(g,arguments[1])}else{var f=this._attributes(arguments[1]);if(f.length){try{d.innerHTML="<"+b+" "+f+">"}catch(h){}g=d.firstChild||null;if(!g){g=document.createElement(b);for(attr in arguments[1]){g[attr=="class"?"className":attr]=arguments[1][attr]}}if(g.tagName.toUpperCase()!=b){g=d.getElementsByTagName(b)[0]}}}}if(arguments[2]){this._children(g,arguments[2])}return $(g)},_text:function(b){return document.createTextNode(b)},ATTR_MAP:{className:"class",htmlFor:"for"},_attributes:function(b){var d=[];for(attribute in b){d.push((attribute in this.ATTR_MAP?this.ATTR_MAP[attribute]:attribute)+'="'+b[attribute].toString().escapeHTML().gsub(/"/,""")+'"')}return d.join(" ")},_children:function(d,b){if(b.tagName){d.appendChild(b);return}if(typeof b=="object"){b.flatten().each(function(f){if(typeof f=="object"){d.appendChild(f)}else{if(Builder._isStringOrNumber(f)){d.appendChild(Builder._text(f))}}})}else{if(Builder._isStringOrNumber(b)){d.appendChild(Builder._text(b))}}},_isStringOrNumber:function(b){return(typeof b=="string"||typeof b=="number")},build:function(d){var b=this.node("div");$(b).update(d.strip());return b.down()},dump:function(d){if(typeof d!="object"&&typeof d!="function"){d=window}var b=("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);b.each(function(e){d[e]=function(){return Builder.node.apply(Builder,[e].concat($A(arguments)))}})}};String.prototype.parseColor=function(){var b="#";if(this.slice(0,4)=="rgb("){var e=this.slice(4,this.length-1).split(",");var d=0;do{b+=parseInt(e[d]).toColorPart()}while(++d<3)}else{if(this.slice(0,1)=="#"){if(this.length==4){for(var d=1;d<4;d++){b+=(this.charAt(d)+this.charAt(d)).toLowerCase()}}if(this.length==7){b=this.toLowerCase()}}}return(b.length==7?b:(arguments[0]||this))};Element.collectTextNodes=function(b){return $A($(b).childNodes).collect(function(d){return(d.nodeType==3?d.nodeValue:(d.hasChildNodes()?Element.collectTextNodes(d):""))}).flatten().join("")};Element.collectTextNodesIgnoreClass=function(b,d){return $A($(b).childNodes).collect(function(e){return(e.nodeType==3?e.nodeValue:((e.hasChildNodes()&&!Element.hasClassName(e,d))?Element.collectTextNodesIgnoreClass(e,d):""))}).flatten().join("")};Element.setContentZoom=function(b,d){b=$(b);b.setStyle({fontSize:(d/100)+"em"});if(Prototype.Browser.WebKit){window.scrollBy(0,0)}return b};Element.getInlineOpacity=function(b){return $(b).style.opacity||""};Element.forceRerendering=function(b){try{b=$(b);var f=document.createTextNode(" ");b.appendChild(f);b.removeChild(f)}catch(d){}};var Effect={_elementDoesNotExistError:{name:"ElementDoesNotExistError",message:"The specified DOM element does not exist, but is required for this effect to operate"},Transitions:{linear:Prototype.K,sinoidal:function(b){return(-Math.cos(b*Math.PI)/2)+0.5},reverse:function(b){return 1-b},flicker:function(b){var b=((-Math.cos(b*Math.PI)/4)+0.75)+Math.random()/4;return b>1?1:b},wobble:function(b){return(-Math.cos(b*Math.PI*(9*b))/2)+0.5},pulse:function(d,b){return(-Math.cos((d*((b||5)-0.5)*2)*Math.PI)/2)+0.5},spring:function(b){return 1-(Math.cos(b*4.5*Math.PI)*Math.exp(-b*6))},none:function(b){return 0},full:function(b){return 1}},DefaultOptions:{duration:1,fps:100,sync:false,from:0,to:1,delay:0,queue:"parallel"},tagifyText:function(b){var d="position:relative";if(Prototype.Browser.IE){d+=";zoom:1"}b=$(b);$A(b.childNodes).each(function(e){if(e.nodeType==3){e.nodeValue.toArray().each(function(f){b.insertBefore(new Element("span",{style:d}).update(f==" "?String.fromCharCode(160):f),e)});Element.remove(e)}})},multiple:function(d,e){var g;if(((typeof d=="object")||Object.isFunction(d))&&(d.length)){g=d}else{g=$(d).childNodes}var b=Object.extend({speed:0.1,delay:0},arguments[2]||{});var f=b.delay;$A(g).each(function(l,h){new e(l,Object.extend(b,{delay:h*b.speed+f}))})},PAIRS:{slide:["SlideDown","SlideUp"],blind:["BlindDown","BlindUp"],appear:["Appear","Fade"]},toggle:function(d,e){d=$(d);e=(e||"appear").toLowerCase();var b=Object.extend({queue:{position:"end",scope:(d.id||"global"),limit:1}},arguments[2]||{});Effect[d.visible()?Effect.PAIRS[e][1]:Effect.PAIRS[e][0]](d,b)}};Effect.DefaultOptions.transition=Effect.Transitions.sinoidal;Effect.ScopedQueue=Class.create(Enumerable,{initialize:function(){this.effects=[];this.interval=null},_each:function(b){this.effects._each(b)},add:function(d){var e=new Date().getTime();var b=Object.isString(d.options.queue)?d.options.queue:d.options.queue.position;switch(b){case"front":this.effects.findAll(function(f){return f.state=="idle"}).each(function(f){f.startOn+=d.finishOn;f.finishOn+=d.finishOn});break;case"with-last":e=this.effects.pluck("startOn").max()||e;break;case"end":e=this.effects.pluck("finishOn").max()||e;break}d.startOn+=e;d.finishOn+=e;if(!d.options.queue.limit||(this.effects.length=this.startOn){if(e>=this.finishOn){this.render(1);this.cancel();this.event("beforeFinish");if(this.finish){this.finish()}this.event("afterFinish");return}var d=(e-this.startOn)/this.totalTime,b=(d*this.totalFrames).round();if(b>this.currentFrame){this.render(d);this.currentFrame=b}}},cancel:function(){if(!this.options.sync){Effect.Queues.get(Object.isString(this.options.queue)?"global":this.options.queue.scope).remove(this)}this.state="finished"},event:function(b){if(this.options[b+"Internal"]){this.options[b+"Internal"](this)}if(this.options[b]){this.options[b](this)}},inspect:function(){var b=$H();for(property in this){if(!Object.isFunction(this[property])){b.set(property,this[property])}}return"#"}});Effect.Parallel=Class.create(Effect.Base,{initialize:function(b){this.effects=b||[];this.start(arguments[1])},update:function(b){this.effects.invoke("render",b)},finish:function(b){this.effects.each(function(d){d.render(1);d.cancel();d.event("beforeFinish");if(d.finish){d.finish(b)}d.event("afterFinish")})}});Effect.Tween=Class.create(Effect.Base,{initialize:function(e,h,g){e=Object.isString(e)?$(e):e;var d=$A(arguments),f=d.last(),b=d.length==5?d[3]:null;this.method=Object.isFunction(f)?f.bind(e):Object.isFunction(e[f])?e[f].bind(e):function(l){e[f]=l};this.start(Object.extend({from:h,to:g},b||{}))},update:function(b){this.method(b)}});Effect.Event=Class.create(Effect.Base,{initialize:function(){this.start(Object.extend({duration:0},arguments[0]||{}))},update:Prototype.emptyFunction});Effect.Opacity=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}if(Prototype.Browser.IE&&(!this.element.currentStyle.hasLayout)){this.element.setStyle({zoom:1})}var b=Object.extend({from:this.element.getOpacity()||0,to:1},arguments[1]||{});this.start(b)},update:function(b){this.element.setOpacity(b)}});Effect.Move=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({x:0,y:0,mode:"relative"},arguments[1]||{});this.start(b)},setup:function(){this.element.makePositioned();this.originalLeft=parseFloat(this.element.getStyle("left")||"0");this.originalTop=parseFloat(this.element.getStyle("top")||"0");if(this.options.mode=="absolute"){this.options.x=this.options.x-this.originalLeft;this.options.y=this.options.y-this.originalTop}},update:function(b){this.element.setStyle({left:(this.options.x*b+this.originalLeft).round()+"px",top:(this.options.y*b+this.originalTop).round()+"px"})}});Effect.MoveBy=function(d,b,e){return new Effect.Move(d,Object.extend({x:e,y:b},arguments[3]||{}))};Effect.Scale=Class.create(Effect.Base,{initialize:function(d,e){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({scaleX:true,scaleY:true,scaleContent:true,scaleFromCenter:false,scaleMode:"box",scaleFrom:100,scaleTo:e},arguments[2]||{});this.start(b)},setup:function(){this.restoreAfterFinish=this.options.restoreAfterFinish||false;this.elementPositioning=this.element.getStyle("position");this.originalStyle={};["top","left","width","height","fontSize"].each(function(d){this.originalStyle[d]=this.element.style[d]}.bind(this));this.originalTop=this.element.offsetTop;this.originalLeft=this.element.offsetLeft;var b=this.element.getStyle("font-size")||"100%";["em","px","%","pt"].each(function(d){if(b.indexOf(d)>0){this.fontSize=parseFloat(b);this.fontSizeType=d}}.bind(this));this.factor=(this.options.scaleTo-this.options.scaleFrom)/100;this.dims=null;if(this.options.scaleMode=="box"){this.dims=[this.element.offsetHeight,this.element.offsetWidth]}if(/^content/.test(this.options.scaleMode)){this.dims=[this.element.scrollHeight,this.element.scrollWidth]}if(!this.dims){this.dims=[this.options.scaleMode.originalHeight,this.options.scaleMode.originalWidth]}},update:function(b){var d=(this.options.scaleFrom/100)+(this.factor*b);if(this.options.scaleContent&&this.fontSize){this.element.setStyle({fontSize:this.fontSize*d+this.fontSizeType})}this.setDimensions(this.dims[0]*d,this.dims[1]*d)},finish:function(b){if(this.restoreAfterFinish){this.element.setStyle(this.originalStyle)}},setDimensions:function(b,g){var h={};if(this.options.scaleX){h.width=g.round()+"px"}if(this.options.scaleY){h.height=b.round()+"px"}if(this.options.scaleFromCenter){var f=(b-this.dims[0])/2;var e=(g-this.dims[1])/2;if(this.elementPositioning=="absolute"){if(this.options.scaleY){h.top=this.originalTop-f+"px"}if(this.options.scaleX){h.left=this.originalLeft-e+"px"}}else{if(this.options.scaleY){h.top=-f+"px"}if(this.options.scaleX){h.left=-e+"px"}}}this.element.setStyle(h)}});Effect.Highlight=Class.create(Effect.Base,{initialize:function(d){this.element=$(d);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({startcolor:"#ffff99"},arguments[1]||{});this.start(b)},setup:function(){if(this.element.getStyle("display")=="none"){this.cancel();return}this.oldStyle={};if(!this.options.keepBackgroundImage){this.oldStyle.backgroundImage=this.element.getStyle("background-image");this.element.setStyle({backgroundImage:"none"})}if(!this.options.endcolor){this.options.endcolor=this.element.getStyle("background-color").parseColor("#ffffff")}if(!this.options.restorecolor){this.options.restorecolor=this.element.getStyle("background-color")}this._base=$R(0,2).map(function(b){return parseInt(this.options.startcolor.slice(b*2+1,b*2+3),16)}.bind(this));this._delta=$R(0,2).map(function(b){return parseInt(this.options.endcolor.slice(b*2+1,b*2+3),16)-this._base[b]}.bind(this))},update:function(b){this.element.setStyle({backgroundColor:$R(0,2).inject("#",function(d,e,f){return d+((this._base[f]+(this._delta[f]*b)).round().toColorPart())}.bind(this))})},finish:function(){this.element.setStyle(Object.extend(this.oldStyle,{backgroundColor:this.options.restorecolor}))}});Effect.ScrollTo=function(e){var d=arguments[1]||{},b=document.viewport.getScrollOffsets(),f=$(e).cumulativeOffset();if(d.offset){f[1]+=d.offset}return new Effect.Tween(null,b.top,f[1],d,function(g){scrollTo(b.left,g.round())})};Effect.Fade=function(e){e=$(e);var b=e.getInlineOpacity();var d=Object.extend({from:e.getOpacity()||1,to:0,afterFinishInternal:function(f){if(f.options.to!=0){return}f.element.hide().setStyle({opacity:b})}},arguments[1]||{});return new Effect.Opacity(e,d)};Effect.Appear=function(d){d=$(d);var b=Object.extend({from:(d.getStyle("display")=="none"?0:d.getOpacity()||0),to:1,afterFinishInternal:function(e){e.element.forceRerendering()},beforeSetup:function(e){e.element.setOpacity(e.options.from).show()}},arguments[1]||{});return new Effect.Opacity(d,b)};Effect.Puff=function(d){d=$(d);var b={opacity:d.getInlineOpacity(),position:d.getStyle("position"),top:d.style.top,left:d.style.left,width:d.style.width,height:d.style.height};return new Effect.Parallel([new Effect.Scale(d,200,{sync:true,scaleFromCenter:true,scaleContent:true,restoreAfterFinish:true}),new Effect.Opacity(d,{sync:true,to:0})],Object.extend({duration:1,beforeSetupInternal:function(e){Position.absolutize(e.effects[0].element)},afterFinishInternal:function(e){e.effects[0].element.hide().setStyle(b)}},arguments[1]||{}))};Effect.BlindUp=function(b){b=$(b);b.makeClipping();return new Effect.Scale(b,0,Object.extend({scaleContent:false,scaleX:false,restoreAfterFinish:true,afterFinishInternal:function(d){d.element.hide().undoClipping()}},arguments[1]||{}))};Effect.BlindDown=function(d){d=$(d);var b=d.getDimensions();return new Effect.Scale(d,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:b.height,originalWidth:b.width},restoreAfterFinish:true,afterSetup:function(e){e.element.makeClipping().setStyle({height:"0px"}).show()},afterFinishInternal:function(e){e.element.undoClipping()}},arguments[1]||{}))};Effect.SwitchOff=function(d){d=$(d);var b=d.getInlineOpacity();return new Effect.Appear(d,Object.extend({duration:0.4,from:0,transition:Effect.Transitions.flicker,afterFinishInternal:function(e){new Effect.Scale(e.element,1,{duration:0.3,scaleFromCenter:true,scaleX:false,scaleContent:false,restoreAfterFinish:true,beforeSetup:function(f){f.element.makePositioned().makeClipping()},afterFinishInternal:function(f){f.element.hide().undoClipping().undoPositioned().setStyle({opacity:b})}})}},arguments[1]||{}))};Effect.DropOut=function(d){d=$(d);var b={top:d.getStyle("top"),left:d.getStyle("left"),opacity:d.getInlineOpacity()};return new Effect.Parallel([new Effect.Move(d,{x:0,y:100,sync:true}),new Effect.Opacity(d,{sync:true,to:0})],Object.extend({duration:0.5,beforeSetup:function(e){e.effects[0].element.makePositioned()},afterFinishInternal:function(e){e.effects[0].element.hide().undoPositioned().setStyle(b)}},arguments[1]||{}))};Effect.Shake=function(f){f=$(f);var d=Object.extend({distance:20,duration:0.5},arguments[1]||{});var g=parseFloat(d.distance);var e=parseFloat(d.duration)/10;var b={top:f.getStyle("top"),left:f.getStyle("left")};return new Effect.Move(f,{x:g,y:0,duration:e,afterFinishInternal:function(h){new Effect.Move(h.element,{x:-g*2,y:0,duration:e*2,afterFinishInternal:function(l){new Effect.Move(l.element,{x:g*2,y:0,duration:e*2,afterFinishInternal:function(n){new Effect.Move(n.element,{x:-g*2,y:0,duration:e*2,afterFinishInternal:function(o){new Effect.Move(o.element,{x:g*2,y:0,duration:e*2,afterFinishInternal:function(p){new Effect.Move(p.element,{x:-g,y:0,duration:e,afterFinishInternal:function(q){q.element.undoPositioned().setStyle(b)}})}})}})}})}})}})};Effect.SlideDown=function(e){e=$(e).cleanWhitespace();var b=e.down().getStyle("bottom");var d=e.getDimensions();return new Effect.Scale(e,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:window.opera?0:1,scaleMode:{originalHeight:d.height,originalWidth:d.width},restoreAfterFinish:true,afterSetup:function(f){f.element.makePositioned();f.element.down().makePositioned();if(window.opera){f.element.setStyle({top:""})}f.element.makeClipping().setStyle({height:"0px"}).show()},afterUpdateInternal:function(f){f.element.down().setStyle({bottom:(f.dims[0]-f.element.clientHeight)+"px"})},afterFinishInternal:function(f){f.element.undoClipping().undoPositioned();f.element.down().undoPositioned().setStyle({bottom:b})}},arguments[1]||{}))};Effect.SlideUp=function(e){e=$(e).cleanWhitespace();var b=e.down().getStyle("bottom");var d=e.getDimensions();return new Effect.Scale(e,window.opera?0:1,Object.extend({scaleContent:false,scaleX:false,scaleMode:"box",scaleFrom:100,scaleMode:{originalHeight:d.height,originalWidth:d.width},restoreAfterFinish:true,afterSetup:function(f){f.element.makePositioned();f.element.down().makePositioned();if(window.opera){f.element.setStyle({top:""})}f.element.makeClipping().show()},afterUpdateInternal:function(f){f.element.down().setStyle({bottom:(f.dims[0]-f.element.clientHeight)+"px"})},afterFinishInternal:function(f){f.element.hide().undoClipping().undoPositioned();f.element.down().undoPositioned().setStyle({bottom:b})}},arguments[1]||{}))};Effect.Squish=function(b){return new Effect.Scale(b,window.opera?1:0,{restoreAfterFinish:true,beforeSetup:function(d){d.element.makeClipping()},afterFinishInternal:function(d){d.element.hide().undoClipping()}})};Effect.Grow=function(e){e=$(e);var d=Object.extend({direction:"center",moveTransition:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.full},arguments[1]||{});var b={top:e.style.top,left:e.style.left,height:e.style.height,width:e.style.width,opacity:e.getInlineOpacity()};var l=e.getDimensions();var n,h;var g,f;switch(d.direction){case"top-left":n=h=g=f=0;break;case"top-right":n=l.width;h=f=0;g=-l.width;break;case"bottom-left":n=g=0;h=l.height;f=-l.height;break;case"bottom-right":n=l.width;h=l.height;g=-l.width;f=-l.height;break;case"center":n=l.width/2;h=l.height/2;g=-l.width/2;f=-l.height/2;break}return new Effect.Move(e,{x:n,y:h,duration:0.01,beforeSetup:function(o){o.element.hide().makeClipping().makePositioned()},afterFinishInternal:function(o){new Effect.Parallel([new Effect.Opacity(o.element,{sync:true,to:1,from:0,transition:d.opacityTransition}),new Effect.Move(o.element,{x:g,y:f,sync:true,transition:d.moveTransition}),new Effect.Scale(o.element,100,{scaleMode:{originalHeight:l.height,originalWidth:l.width},sync:true,scaleFrom:window.opera?1:0,transition:d.scaleTransition,restoreAfterFinish:true})],Object.extend({beforeSetup:function(p){p.effects[0].element.setStyle({height:"0px"}).show()},afterFinishInternal:function(p){p.effects[0].element.undoClipping().undoPositioned().setStyle(b)}},d))}})};Effect.Shrink=function(e){e=$(e);var d=Object.extend({direction:"center",moveTransition:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.none},arguments[1]||{});var b={top:e.style.top,left:e.style.left,height:e.style.height,width:e.style.width,opacity:e.getInlineOpacity()};var h=e.getDimensions();var g,f;switch(d.direction){case"top-left":g=f=0;break;case"top-right":g=h.width;f=0;break;case"bottom-left":g=0;f=h.height;break;case"bottom-right":g=h.width;f=h.height;break;case"center":g=h.width/2;f=h.height/2;break}return new Effect.Parallel([new Effect.Opacity(e,{sync:true,to:0,from:1,transition:d.opacityTransition}),new Effect.Scale(e,window.opera?1:0,{sync:true,transition:d.scaleTransition,restoreAfterFinish:true}),new Effect.Move(e,{x:g,y:f,sync:true,transition:d.moveTransition})],Object.extend({beforeStartInternal:function(l){l.effects[0].element.makePositioned().makeClipping()},afterFinishInternal:function(l){l.effects[0].element.hide().undoClipping().undoPositioned().setStyle(b)}},d))};Effect.Pulsate=function(e){e=$(e);var d=arguments[1]||{},b=e.getInlineOpacity(),g=d.transition||Effect.Transitions.linear,f=function(h){return 1-g((-Math.cos((h*(d.pulses||5)*2)*Math.PI)/2)+0.5)};return new Effect.Opacity(e,Object.extend(Object.extend({duration:2,from:0,afterFinishInternal:function(h){h.element.setStyle({opacity:b})}},d),{transition:f}))};Effect.Fold=function(d){d=$(d);var b={top:d.style.top,left:d.style.left,width:d.style.width,height:d.style.height};d.makeClipping();return new Effect.Scale(d,5,Object.extend({scaleContent:false,scaleX:false,afterFinishInternal:function(e){new Effect.Scale(d,1,{scaleContent:false,scaleY:false,afterFinishInternal:function(f){f.element.hide().undoClipping().setStyle(b)}})}},arguments[1]||{}))};Effect.Morph=Class.create(Effect.Base,{initialize:function(e){this.element=$(e);if(!this.element){throw (Effect._elementDoesNotExistError)}var b=Object.extend({style:{}},arguments[1]||{});if(!Object.isString(b.style)){this.style=$H(b.style)}else{if(b.style.include(":")){this.style=b.style.parseStyle()}else{this.element.addClassName(b.style);this.style=$H(this.element.getStyles());this.element.removeClassName(b.style);var d=this.element.getStyles();this.style=this.style.reject(function(f){return f.value==d[f.key]});b.afterFinishInternal=function(f){f.element.addClassName(f.options.style);f.transforms.each(function(g){f.element.style[g.style]=""})}}}this.start(b)},setup:function(){function b(d){if(!d||["rgba(0, 0, 0, 0)","transparent"].include(d)){d="#ffffff"}d=d.parseColor();return $R(0,2).map(function(e){return parseInt(d.slice(e*2+1,e*2+3),16)})}this.transforms=this.style.map(function(l){var h=l[0],g=l[1],f=null;if(g.parseColor("#zzzzzz")!="#zzzzzz"){g=g.parseColor();f="color"}else{if(h=="opacity"){g=parseFloat(g);if(Prototype.Browser.IE&&(!this.element.currentStyle.hasLayout)){this.element.setStyle({zoom:1})}}else{if(Element.CSS_LENGTH.test(g)){var e=g.match(/^([\+\-]?[0-9\.]+)(.*)$/);g=parseFloat(e[1]);f=(e.length==3)?e[2]:null}}}var d=this.element.getStyle(h);return{style:h.camelize(),originalValue:f=="color"?b(d):parseFloat(d||0),targetValue:f=="color"?b(g):g,unit:f}}.bind(this)).reject(function(d){return((d.originalValue==d.targetValue)||(d.unit!="color"&&(isNaN(d.originalValue)||isNaN(d.targetValue))))})},update:function(b){var f={},d,e=this.transforms.length;while(e--){f[(d=this.transforms[e]).style]=d.unit=="color"?"#"+(Math.round(d.originalValue[0]+(d.targetValue[0]-d.originalValue[0])*b)).toColorPart()+(Math.round(d.originalValue[1]+(d.targetValue[1]-d.originalValue[1])*b)).toColorPart()+(Math.round(d.originalValue[2]+(d.targetValue[2]-d.originalValue[2])*b)).toColorPart():(d.originalValue+(d.targetValue-d.originalValue)*b).toFixed(3)+(d.unit===null?"":d.unit)}this.element.setStyle(f,true)}});Effect.Transform=Class.create({initialize:function(b){this.tracks=[];this.options=arguments[1]||{};this.addTracks(b)},addTracks:function(b){b.each(function(d){d=$H(d);var e=d.values().first();this.tracks.push($H({ids:d.keys().first(),effect:Effect.Morph,options:{style:e}}))}.bind(this));return this},play:function(){return new Effect.Parallel(this.tracks.map(function(b){var f=b.get("ids"),e=b.get("effect"),d=b.get("options");var g=[$(f)||$$(f)].flatten();return g.map(function(h){return new e(h,Object.extend({sync:true},d))})}).flatten(),this.options)}});Element.CSS_PROPERTIES=$w("backgroundColor backgroundPosition borderBottomColor borderBottomStyle borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth borderRightColor borderRightStyle borderRightWidth borderSpacing borderTopColor borderTopStyle borderTopWidth bottom clip color fontSize fontWeight height left letterSpacing lineHeight marginBottom marginLeft marginRight marginTop markerOffset maxHeight maxWidth minHeight minWidth opacity outlineColor outlineOffset outlineWidth paddingBottom paddingLeft paddingRight paddingTop right textIndent top width wordSpacing zIndex");Element.CSS_LENGTH=/^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;String.__parseStyleElement=document.createElement("div");String.prototype.parseStyle=function(){var d,b=$H();if(Prototype.Browser.WebKit){d=new Element("div",{style:this}).style}else{String.__parseStyleElement.innerHTML='
';d=String.__parseStyleElement.childNodes[0].style}Element.CSS_PROPERTIES.each(function(e){if(d[e]){b.set(e,d[e])}});if(Prototype.Browser.IE&&this.include("opacity")){b.set("opacity",this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1])}return b};if(document.defaultView&&document.defaultView.getComputedStyle){Element.getStyles=function(d){var b=document.defaultView.getComputedStyle($(d),null);return Element.CSS_PROPERTIES.inject({},function(e,f){e[f]=b[f];return e})}}else{Element.getStyles=function(d){d=$(d);var b=d.currentStyle,e;e=Element.CSS_PROPERTIES.inject({},function(f,g){f[g]=b[g];return f});if(!e.opacity){e.opacity=d.getOpacity()}return e}}Effect.Methods={morph:function(b,d){b=$(b);new Effect.Morph(b,Object.extend({style:d},arguments[2]||{}));return b},visualEffect:function(e,g,d){e=$(e);var f=g.dasherize().camelize(),b=f.charAt(0).toUpperCase()+f.substring(1);new Effect[b](e,d);return e},highlight:function(d,b){d=$(d);new Effect.Highlight(d,b);return d}};$w("fade appear grow shrink fold blindUp blindDown slideUp slideDown pulsate shake puff squish switchOff dropOut").each(function(b){Effect.Methods[b]=function(e,d){e=$(e);Effect[b.charAt(0).toUpperCase()+b.substring(1)](e,d);return e}});$w("getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles").each(function(b){Effect.Methods[b]=Element[b]});Element.addMethods(Effect.Methods);function validateCreditCard(e){var d="0123456789";var b="";for(i=0;i9?Math.floor(a/10+a%10):a}for(i=0;i=d},maxLength:function(b,e,d){return b.length<=d},min:function(b,e,d){return b>=parseFloat(d)},max:function(b,e,d){return b<=parseFloat(d)},notOneOf:function(b,e,d){return $A(d).all(function(f){return b!=f})},oneOf:function(b,e,d){return $A(d).any(function(f){return b==f})},is:function(b,e,d){return b==d},isNot:function(b,e,d){return b!=d},equalToField:function(b,e,d){return b==$F(d)},notEqualToField:function(b,e,d){return b!=$F(d)},include:function(b,e,d){return $A(d).all(function(f){return Validation.get(f).test(b,e)})}};var Validation=Class.create();Validation.defaultOptions={onSubmit:true,stopOnFirst:false,immediate:false,focusOnError:true,useTitles:false,addClassNameToContainer:false,containerClassName:".input-box",onFormValidate:function(b,d){},onElementValidate:function(b,d){}};Validation.prototype={initialize:function(d,b){this.form=$(d);if(!this.form){return}this.options=Object.extend({onSubmit:Validation.defaultOptions.onSubmit,stopOnFirst:Validation.defaultOptions.stopOnFirst,immediate:Validation.defaultOptions.immediate,focusOnError:Validation.defaultOptions.focusOnError,useTitles:Validation.defaultOptions.useTitles,onFormValidate:Validation.defaultOptions.onFormValidate,onElementValidate:Validation.defaultOptions.onElementValidate},b||{});if(this.options.onSubmit){Event.observe(this.form,"submit",this.onSubmit.bind(this),false)}if(this.options.immediate){Form.getElements(this.form).each(function(e){if(e.tagName.toLowerCase()=="select"){Event.observe(e,"blur",this.onChange.bindAsEventListener(this))}if(e.type.toLowerCase()=="radio"||e.type.toLowerCase()=="checkbox"){Event.observe(e,"click",this.onChange.bindAsEventListener(this))}else{Event.observe(e,"change",this.onChange.bindAsEventListener(this))}},this)}},onChange:function(b){Validation.isOnChange=true;Validation.validate(Event.element(b),{useTitle:this.options.useTitles,onElementValidate:this.options.onElementValidate});Validation.isOnChange=false},onSubmit:function(b){if(!this.validate()){Event.stop(b)}},validate:function(){var b=false;var d=this.options.useTitles;var g=this.options.onElementValidate;try{if(this.options.stopOnFirst){b=Form.getElements(this.form).all(function(e){if(e.hasClassName("local-validation")&&!this.isElementInForm(e,this.form)){return true}return Validation.validate(e,{useTitle:d,onElementValidate:g})},this)}else{b=Form.getElements(this.form).collect(function(e){if(e.hasClassName("local-validation")&&!this.isElementInForm(e,this.form)){return true}if(e.hasClassName("validation-disabled")){return true}return Validation.validate(e,{useTitle:d,onElementValidate:g})},this).all()}}catch(f){}if(!b&&this.options.focusOnError){try{Form.getElements(this.form).findAll(function(e){return $(e).hasClassName("validation-failed")}).first().focus()}catch(f){}}this.options.onFormValidate(b,this.form);return b},reset:function(){Form.getElements(this.form).each(Validation.reset)},isElementInForm:function(e,d){var b=e.up("form");if(b==d){return true}return false}};Object.extend(Validation,{validate:function(e,b){b=Object.extend({useTitle:false,onElementValidate:function(f,g){}},b||{});e=$(e);var d=$w(e.className);return result=d.all(function(f){var g=Validation.test(f,e,b.useTitle);b.onElementValidate(g,e);return g})},insertAdvice:function(f,d){var b=$(f).up(".field-row");if(b){Element.insert(b,{after:d})}else{if(f.up("td.value")){f.up("td.value").insert({bottom:d})}else{if(f.advaiceContainer&&$(f.advaiceContainer)){$(f.advaiceContainer).update(d)}else{switch(f.type.toLowerCase()){case"checkbox":case"radio":var e=f.parentNode;if(e){Element.insert(e,{bottom:d})}else{Element.insert(f,{after:d})}break;default:Element.insert(f,{after:d})}}}}},showAdvice:function(e,d,b){if(!e.advices){e.advices=new Hash()}else{e.advices.each(function(f){if(!d||f.value.id!=d.id){this.hideAdvice(e,f.value)}}.bind(this))}e.advices.set(b,d);if(typeof Effect=="undefined"){d.style.display="block"}else{if(!d._adviceAbsolutize){new Effect.Appear(d,{duration:1})}else{Position.absolutize(d);d.show();d.setStyle({top:d._adviceTop,left:d._adviceLeft,width:d._adviceWidth,"z-index":1000});d.addClassName("advice-absolute")}}},hideAdvice:function(d,b){if(b!=null){new Effect.Fade(b,{duration:1,afterFinishInternal:function(){b.hide()}})}},updateCallback:function(elm,status){if(typeof elm.callbackFunction!="undefined"){eval(elm.callbackFunction+"('"+elm.id+"','"+status+"')")}},ajaxError:function(g,f){var e="validate-ajax";var d=Validation.getAdvice(e,g);if(d==null){d=this.createAdvice(e,g,false,f)}this.showAdvice(g,d,"validate-ajax");this.updateCallback(g,"failed");g.addClassName("validation-failed");g.addClassName("validate-ajax");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=g.up(Validation.defaultOptions.containerClassName);if(b&&this.allowContainerClassName(g)){b.removeClassName("validation-passed");b.addClassName("validation-error")}}},allowContainerClassName:function(b){if(b.type=="radio"||b.type=="checkbox"){return b.hasClassName("change-container-classname")}return true},test:function(g,o,l){var d=Validation.get(g);var n="__advice"+g.camelize();try{if(Validation.isVisible(o)&&!d.test($F(o),o)){var f=Validation.getAdvice(g,o);if(f==null){f=this.createAdvice(g,o,l)}this.showAdvice(o,f,g);this.updateCallback(o,"failed");o[n]=1;if(!o.advaiceContainer){o.removeClassName("validation-passed");o.addClassName("validation-failed")}if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=o.up(Validation.defaultOptions.containerClassName);if(b&&this.allowContainerClassName(o)){b.removeClassName("validation-passed");b.addClassName("validation-error")}}return false}else{var f=Validation.getAdvice(g,o);this.hideAdvice(o,f);this.updateCallback(o,"passed");o[n]="";o.removeClassName("validation-failed");o.addClassName("validation-passed");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var b=o.up(Validation.defaultOptions.containerClassName);if(b&&!b.down(".validation-failed")&&this.allowContainerClassName(o)){if(!Validation.get("IsEmpty").test(o.value)||!this.isVisible(o)){b.addClassName("validation-passed")}else{b.removeClassName("validation-passed")}b.removeClassName("validation-error")}}return true}}catch(h){throw (h)}},isVisible:function(b){while(b.tagName!="BODY"){if(!$(b).visible()){return false}b=b.parentNode}return true},getAdvice:function(b,d){return $("advice-"+b+"-"+Validation.getElmID(d))||$("advice-"+Validation.getElmID(d))},createAdvice:function(e,n,l,d){var b=Validation.get(e);var h=l?((n&&n.title)?n.title:b.error):b.error;if(d){h=d}if(jQuery.mage.__){h=jQuery.mage.__(h)}advice='";Validation.insertAdvice(n,advice);advice=Validation.getAdvice(e,n);if($(n).hasClassName("absolute-advice")){var g=$(n).getDimensions();var f=Position.cumulativeOffset(n);advice._adviceTop=(f[1]+g.height)+"px";advice._adviceLeft=(f[0])+"px";advice._adviceWidth=(g.width)+"px";advice._adviceAbsolutize=true}return advice},getElmID:function(b){return b.id?b.id:b.name},reset:function(d){d=$(d);var b=$w(d.className);b.each(function(g){var h="__advice"+g.camelize();if(d[h]){var f=Validation.getAdvice(g,d);if(f){f.hide()}d[h]=""}d.removeClassName("validation-failed");d.removeClassName("validation-passed");if(Validation.defaultOptions.addClassNameToContainer&&Validation.defaultOptions.containerClassName!=""){var e=d.up(Validation.defaultOptions.containerClassName);if(e){e.removeClassName("validation-passed");e.removeClassName("validation-error")}}})},add:function(f,e,g,d){var b={};b[f]=new Validator(f,e,g,d);Object.extend(Validation.methods,b)},addAllThese:function(b){var d={};$A(b).each(function(e){d[e[0]]=new Validator(e[0],e[1],e[2],(e.length>3?e[3]:{}))});Object.extend(Validation.methods,d)},get:function(b){return Validation.methods[b]?Validation.methods[b]:Validation.methods._LikeNoIDIEverSaw_},methods:{_LikeNoIDIEverSaw_:new Validator("_LikeNoIDIEverSaw_","",{})}});Validation.add("IsEmpty","",function(b){return(b==""||(b==null)||(b.length==0)||/^\s+$/.test(b))});Validation.addAllThese([["validate-no-html-tags","HTML tags are not allowed",function(b){return !/<(\/)?\w+/.test(b)}],["validate-select","Please select an option.",function(b){return((b!="none")&&(b!=null)&&(b.length!=0))}],["required-entry","This is a required field.",function(b){return !Validation.get("IsEmpty").test(b)}],["validate-number","Please enter a valid number in this field.",function(b){return Validation.get("IsEmpty").test(b)||(!isNaN(parseNumber(b))&&/^\s*-?\d*(\.\d*)?\s*$/.test(b))}],["validate-number-range","The value is not within the specified range.",function(e,g){if(Validation.get("IsEmpty").test(e)){return true}var f=parseNumber(e);if(isNaN(f)){return false}var d=/^number-range-(-?[\d.,]+)?-(-?[\d.,]+)?$/,b=true;$w(g.className).each(function(l){var h=d.exec(l);if(h){b=b&&(h[1]==null||h[1]==""||f>=parseNumber(h[1]))&&(h[2]==null||h[2]==""||f<=parseNumber(h[2]))}});return b}],["validate-digits","Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.",function(b){return Validation.get("IsEmpty").test(b)||!/[^\d]/.test(b)}],["validate-digits-range","The value is not within the specified range.",function(e,g){if(Validation.get("IsEmpty").test(e)){return true}var f=parseNumber(e);if(isNaN(f)){return false}var d=/^digits-range-(-?\d+)?-(-?\d+)?$/,b=true;$w(g.className).each(function(l){var h=d.exec(l);if(h){b=b&&(h[1]==null||h[1]==""||f>=parseNumber(h[1]))&&(h[2]==null||h[2]==""||f<=parseNumber(h[2]))}});return b}],["validate-range","The value is not within the specified range.",function(f,l){var g,h;if(Validation.get("IsEmpty").test(f)){return true}else{if(Validation.get("validate-digits").test(f)){g=h=parseNumber(f)}else{var e=/^(-?\d+)?-(-?\d+)?$/.exec(f);if(e){g=parseNumber(e[1]);h=parseNumber(e[2]);if(g>h){return false}}else{return false}}}var d=/^range-(-?\d+)?-(-?\d+)?$/,b=true;$w(l.className).each(function(n){var q=d.exec(n);if(q){var p=parseNumber(q[1]);var o=parseNumber(q[2]);b=b&&(isNaN(p)||g>=p)&&(isNaN(o)||h<=o)}});return b}],["validate-alpha","Please use letters only (a-z or A-Z) in this field.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z]+$/.test(b)}],["validate-code","Please use only letters (a-z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-z]+[a-z0-9_]+$/.test(b)}],["validate-alphanum","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z0-9]+$/.test(b)}],["validate-alphanum-with-spaces","Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field.",function(b){return Validation.get("IsEmpty").test(b)||/^[a-zA-Z0-9 ]+$/.test(b)}],["validate-street",'Please use only letters (a-z or A-Z), numbers (0-9), spaces and "#" in this field.',function(b){return Validation.get("IsEmpty").test(b)||/^[ \w]{3,}([A-Za-z]\.)?([ \w]*\#\d+)?(\r\n| )[ \w]{3,}/.test(b)}],["validate-phoneStrict","Please enter a valid phone number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^(\()?\d{3}(\))?(-|\s)?\d{3}(-|\s)\d{4}$/.test(b)}],["validate-phoneLax","Please enter a valid phone number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^((\d[-. ]?)?((\(\d{3}\))|\d{3}))?[-. ]?\d{3}[-. ]?\d{4}$/.test(b)}],["validate-fax","Please enter a valid fax number (Ex: 123-456-7890).",function(b){return Validation.get("IsEmpty").test(b)||/^(\()?\d{3}(\))?(-|\s)?\d{3}(-|\s)\d{4}$/.test(b)}],["validate-date","Please enter a valid date.",function(b){var d=new Date(b);return Validation.get("IsEmpty").test(b)||!isNaN(d)}],["validate-date-range","Make sure the To Date is later than or the same as the From Date.",function(e,h){var d=/\bdate-range-(\w+)-(\w+)\b/.exec(h.className);if(!d||d[2]=="to"||Validation.get("IsEmpty").test(e)){return true}var f=new Date().getFullYear()+"";var b=function(l){l=l.split(/[.\/]/);if(l[2]&&l[2].length<4){l[2]=f.substr(0,l[2].length)+l[2]}return new Date(l.join("/")).getTime()};var g=Element.select(h.form,".validate-date-range.date-range-"+d[1]+"-to");return !g.length||Validation.get("IsEmpty").test(g[0].value)||b(e)<=b(g[0].value)}],["validate-email","Please enter a valid email address (Ex: johndoe@domain.com).",function(b){return Validation.get("IsEmpty").test(b)||/^([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*@([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*\.(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,})$/i.test(b)}],["validate-emailSender","Please use only visible characters and spaces.",function(b){return Validation.get("IsEmpty").test(b)||/^[\S ]+$/.test(b)}],["validate-password","Please enter 6 or more characters. Leading and trailing spaces will be ignored.",function(b){var d=b.strip();return !(d.length>0&&d.length<6)}],["validate-admin-password","Please enter 7 or more characters, using both numeric and alphabetic.",function(b){var d=b.strip();if(0==d.length){return true}if(!(/[a-z]/i.test(b))||!(/[0-9]/.test(b))){return false}return !(d.length<7)}],["validate-cpassword","Please make sure your passwords match.",function(b){var d=$("confirmation")?$("confirmation"):$$(".validate-cpassword")[0];var g=false;if($("password")){g=$("password")}var h=$$(".validate-password");for(var e=0;e=0}],["validate-zero-or-greater","Please enter a number 0 or greater in this field.",function(b){return Validation.get("validate-not-negative-number").test(b)}],["validate-greater-than-zero","Please enter a number greater than 0 in this field.",function(b){if(Validation.get("IsEmpty").test(b)){return true}b=parseNumber(b);return !isNaN(b)&&b>0}],["validate-state","Please select State/Province.",function(b){return(b!=0||b=="")}],["validate-new-password","Please enter 6 or more characters. Leading and trailing spaces will be ignored.",function(b){if(!Validation.get("validate-password").test(b)){return false}if(Validation.get("IsEmpty").test(b)&&b!=""){return false}return true}],["validate-cc-number","Please enter a valid credit card number.",function(b,e){var d=$(e.id.substr(0,e.id.indexOf("_cc_number"))+"_cc_type");if(d&&typeof Validation.creditCartTypes.get(d.value)!="undefined"&&Validation.creditCartTypes.get(d.value)[2]==false){if(!Validation.get("IsEmpty").test(b)&&Validation.get("validate-digits").test(b)){return true}else{return false}}return validateCreditCard(b)}],["validate-cc-type","Credit card number does not match credit card type.",function(d,g){g.value=removeDelimiters(g.value);d=removeDelimiters(d);var f=$(g.id.substr(0,g.id.indexOf("_cc_number"))+"_cc_type");if(!f){return true}var e=f.value;if(typeof Validation.creditCartTypes.get(e)=="undefined"){return false}if(Validation.creditCartTypes.get(e)[0]==false){return true}var b="";Validation.creditCartTypes.each(function(h){if(h.value[0]&&d.match(h.value[0])){b=h.key;throw $break}});if(b!=e){return false}if(f.hasClassName("validation-failed")&&Validation.isOnChange){Validation.validate(f)}return true}],["validate-cc-type-select","Card type does not match credit card number.",function(d,e){var b=$(e.id.substr(0,e.id.indexOf("_cc_type"))+"_cc_number");if(Validation.isOnChange&&Validation.get("IsEmpty").test(b.value)){return true}if(Validation.get("validate-cc-type").test(b.value,b)){Validation.validate(b)}return Validation.get("validate-cc-type").test(b.value,b)}],["validate-cc-exp","Incorrect credit card expiration date.",function(b,l){var h=b;var g=$(l.id.substr(0,l.id.indexOf("_expiration"))+"_expiration_yr").value;var f=new Date();var e=f.getMonth()+1;var d=f.getFullYear();if(h=n)}});return b}],["validate-percents","Please enter a number lower than 100.",{max:100}],["required-file","Please select a file.",function(d,e){var b=!Validation.get("IsEmpty").test(d);if(b===false){ovId=e.id+"_value";if($(ovId)){b=!Validation.get("IsEmpty").test($(ovId).value)}}return b}],["validate-cc-ukss","Please enter issue number or start date for switch/solo card type.",function(o,g){var b;if(g.id.match(/(.)+_cc_issue$/)){b=g.id.indexOf("_cc_issue")}else{if(g.id.match(/(.)+_start_month$/)){b=g.id.indexOf("_start_month")}else{b=g.id.indexOf("_start_year")}}var f=g.id.substr(0,b);var d=$(f+"_cc_type");if(!d){return true}var n=d.value;if(["SS","SM","SO"].indexOf(n)==-1){return true}$(f+"_cc_issue").advaiceContainer=$(f+"_start_month").advaiceContainer=$(f+"_start_year").advaiceContainer=$(f+"_cc_type_ss_div").down(".adv-container");var h=$(f+"_cc_issue").value;var l=$(f+"_start_month").value;var p=$(f+"_start_year").value;var e=(l&&p)?true:false;if(!e&&!h){return false}return true}]]);function removeDelimiters(b){b=b.replace(/\s/g,"");b=b.replace(/\-/g,"");return b}function parseNumber(b){if(typeof b!="string"){return parseFloat(b)}var e=b.indexOf(".");var d=b.indexOf(",");if(e!=-1&&d!=-1){if(d>e){b=b.replace(".","").replace(",",".")}else{b=b.replace(",","")}}else{if(d!=-1){b=b.replace(",",".")}}return parseFloat(b)}Validation.creditCartTypes=$H({SO:[new RegExp("^(6334[5-9]([0-9]{11}|[0-9]{13,14}))|(6767([0-9]{12}|[0-9]{14,15}))$"),new RegExp("^([0-9]{3}|[0-9]{4})?$"),true],SM:[new RegExp("(^(5[0678])[0-9]{11,18}$)|(^(6[^05])[0-9]{11,18}$)|(^(601)[^1][0-9]{9,16}$)|(^(6011)[0-9]{9,11}$)|(^(6011)[0-9]{13,16}$)|(^(65)[0-9]{11,13}$)|(^(65)[0-9]{15,18}$)|(^(49030)[2-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49033)[5-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49110)[1-2]([0-9]{10}$|[0-9]{12,13}$))|(^(49117)[4-9]([0-9]{10}$|[0-9]{12,13}$))|(^(49118)[0-2]([0-9]{10}$|[0-9]{12,13}$))|(^(4936)([0-9]{12}$|[0-9]{14,15}$))"),new RegExp("^([0-9]{3}|[0-9]{4})?$"),true],VI:[new RegExp("^4[0-9]{12}([0-9]{3})?$"),new RegExp("^[0-9]{3}$"),true],MC:[new RegExp("^5[1-5][0-9]{14}$"),new RegExp("^[0-9]{3}$"),true],AE:[new RegExp("^3[47][0-9]{13}$"),new RegExp("^[0-9]{4}$"),true],DI:[new RegExp("^6(011|4[4-9][0-9]|5[0-9]{2})[0-9]{12}$"),new RegExp("^[0-9]{3}$"),true],JCB:[new RegExp("^(3[0-9]{15}|(2131|1800)[0-9]{11})$"),new RegExp("^[0-9]{3,4}$"),true],OT:[false,new RegExp("^([0-9]{3}|[0-9]{4})?$"),false]});function popWin(d,e,b){var e=window.open(d,e,b);e.focus()}function setLocation(b){window.location.href=b}function setPLocation(d,b){if(b){window.opener.focus()}window.opener.location.href=d}function setLanguageCode(e,f){var b=window.location.href;var h="",g;if(g=b.match(/\#(.*)$/)){b=b.replace(/\#(.*)$/,"");h=g[0]}if(b.match(/[?]/)){var d=/([?&]store=)[a-z0-9_]*/;if(b.match(d)){b=b.replace(d,"$1"+e)}else{b+="&store="+e}var d=/([?&]from_store=)[a-z0-9_]*/;if(b.match(d)){b=b.replace(d,"")}}else{b+="?store="+e}if(typeof(f)!="undefined"){b+="&from_store="+f}b+=h;setLocation(b)}function decorateGeneric(h,e){var l=["odd","even","first","last"];var d={};var g=h.length;if(g){if(typeof(e)=="undefined"){e=l}if(!e.length){return}for(var b in l){d[l[b]]=false}for(var b in e){d[e[b]]=true}if(d.first){Element.addClassName(h[0],"first")}if(d.last){Element.addClassName(h[g-1],"last")}for(var f=0;f-1){b="?"+f.substring(d+2);f=f.substring(0,d+1)}return f+e+b}function formatCurrency(n,q,g){var l=isNaN(q.precision=Math.abs(q.precision))?2:q.precision;var v=isNaN(q.requiredPrecision=Math.abs(q.requiredPrecision))?2:q.requiredPrecision;l=v;var t=isNaN(q.integerRequired=Math.abs(q.integerRequired))?1:q.integerRequired;var p=q.decimalSymbol==undefined?",":q.decimalSymbol;var e=q.groupSymbol==undefined?".":q.groupSymbol;var d=q.groupLength==undefined?3:q.groupLength;var u="";if(g==undefined||g==true){u=n<0?"-":(g?"+":"")}else{if(g==false){u=""}}var h=parseInt(n=Math.abs(+n||0).toFixed(l))+"";var f=(h.lengthd?j%d:0;re=new RegExp("(\\d{"+d+"})(?=\\d)","g");var b=(j?h.substr(0,j)+e:"")+h.substr(j).replace(re,"$1"+e)+(l?p+Math.abs(n-h).toFixed(l).replace(/-/,0).slice(2):"");var o="";if(q.pattern.indexOf("{sign}")==-1){o=u+q.pattern}else{o=q.pattern.replace("{sign}",u)}return o.replace("%s",b).replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function expandDetails(d,b){if(Element.hasClassName(d,"show-details")){$$(b).each(function(e){e.hide()});Element.removeClassName(d,"show-details")}else{$$(b).each(function(e){e.show()});Element.addClassName(d,"show-details")}}var isIE=navigator.appVersion.match(/MSIE/)=="MSIE";if(!window.Varien){var Varien=new Object()}Varien.showLoading=function(){var b=$("loading-process");b&&b.show()};Varien.hideLoading=function(){var b=$("loading-process");b&&b.hide()};Varien.GlobalHandlers={onCreate:function(){Varien.showLoading()},onComplete:function(){if(Ajax.activeRequestCount==0){Varien.hideLoading()}}};Ajax.Responders.register(Varien.GlobalHandlers);Varien.searchForm=Class.create();Varien.searchForm.prototype={initialize:function(d,e,b){this.form=$(d);this.field=$(e);this.emptyText=b;Event.observe(this.form,"submit",this.submit.bind(this));Event.observe(this.field,"focus",this.focus.bind(this));Event.observe(this.field,"blur",this.blur.bind(this));this.blur()},submit:function(b){if(this.field.value==this.emptyText||this.field.value==""){Event.stop(b);return false}return true},focus:function(b){if(this.field.value==this.emptyText){this.field.value=""}},blur:function(b){if(this.field.value==""){this.field.value=this.emptyText}}};Varien.DateElement=Class.create();Varien.DateElement.prototype={initialize:function(b,d,f,e){if(b=="id"){this.day=$(d+"day");this.month=$(d+"month");this.year=$(d+"year");this.full=$(d+"full");this.advice=$(d+"date-advice")}else{if(b=="container"){this.day=d.day;this.month=d.month;this.year=d.year;this.full=d.full;this.advice=d.advice}else{return}}this.required=f;this.format=e;this.day.addClassName("validate-custom");this.day.validate=this.validate.bind(this);this.month.addClassName("validate-custom");this.month.validate=this.validate.bind(this);this.year.addClassName("validate-custom");this.year.validate=this.validate.bind(this);this.setDateRange(false,false);this.year.setAttribute("autocomplete","off");this.advice.hide()},validate:function(){var l=false,o=parseInt(this.day.value,10)||0,f=parseInt(this.month.value,10)||0,h=parseInt(this.year.value,10)||0;if(this.day.value.strip().empty()&&this.month.value.strip().empty()&&this.year.value.strip().empty()){if(this.required){l="Please enter a date."}else{this.full.value=""}}else{if(!o||!f||!h){l="Please enter a valid full date."}else{var d=new Date,n=0,e=null;d.setYear(h);d.setMonth(f-1);d.setDate(32);n=32-d.getDate();if(!n||n>31){n=31}if(o<1||o>n){e="day";l="Please enter a valid day (1-%1)."}else{if(f<1||f>12){e="month";l="Please enter a valid month (1-12)."}else{if(o%10==o){this.day.value="0"+o}if(f%10==f){this.month.value="0"+f}this.full.value=this.format.replace(/%[mb]/i,this.month.value).replace(/%[de]/i,this.day.value).replace(/%y/i,this.year.value);var b=this.month.value+"/"+this.day.value+"/"+this.year.value;var g=new Date(b);if(isNaN(g)){l="Please enter a valid date."}else{this.setFullDate(g)}}}var p=false;if(!l&&!this.validateData()){e=this.validateDataErrorType;p=this.validateDataErrorText;l=p}}}if(l!==false){if(jQuery.mage.__){l=jQuery.mage.__(l)}if(!p){this.advice.innerHTML=l.replace("%1",n)}else{this.advice.innerHTML=this.errorTextModifier(l)}this.advice.show();return false}this.day.removeClassName("validation-failed");this.month.removeClassName("validation-failed");this.year.removeClassName("validation-failed");this.advice.hide();return true},validateData:function(){var d=this.fullDate.getFullYear();var b=new Date;this.curyear=b.getFullYear();return(d>=1900&&d<=this.curyear)},validateDataErrorType:"year",validateDataErrorText:"Please enter a valid year (1900-%1).",errorTextModifier:function(b){return b.replace("%1",this.curyear)},setDateRange:function(b,d){this.minDate=b;this.maxDate=d},setFullDate:function(b){this.fullDate=b}};Varien.DOB=Class.create();Varien.DOB.prototype={initialize:function(b,g,f){var e=$$(b)[0];var d={};d.day=Element.select(e,".dob-day input")[0];d.month=Element.select(e,".dob-month input")[0];d.year=Element.select(e,".dob-year input")[0];d.full=Element.select(e,".dob-full input")[0];d.advice=Element.select(e,".validation-advice")[0];new Varien.DateElement("container",d,g,f)}};Varien.dateRangeDate=Class.create();Varien.dateRangeDate.prototype=Object.extend(new Varien.DateElement(),{validateData:function(){var b=true;if(this.minDate||this.maxValue){if(this.minDate){this.minDate=new Date(this.minDate);this.minDate.setHours(0);if(isNaN(this.minDate)){this.minDate=new Date("1/1/1900")}b=b&&(this.fullDate>=this.minDate)}if(this.maxDate){this.maxDate=new Date(this.maxDate);this.minDate.setHours(0);if(isNaN(this.maxDate)){this.maxDate=new Date()}b=b&&(this.fullDate<=this.maxDate)}if(this.maxDate&&this.minDate){this.validateDataErrorText="Please enter a valid date between %s and %s"}else{if(this.maxDate){this.validateDataErrorText="Please enter a valid date less than or equal to %s"}else{if(this.minDate){this.validateDataErrorText="Please enter a valid date equal to or greater than %s"}else{this.validateDataErrorText=""}}}}return b},validateDataErrorText:"Date should be between %s and %s",errorTextModifier:function(b){if(this.minDate){b=b.sub("%s",this.dateFormat(this.minDate))}if(this.maxDate){b=b.sub("%s",this.dateFormat(this.maxDate))}return b},dateFormat:function(b){return(b.getMonth()+1)+"/"+b.getDate()+"/"+b.getFullYear()}});Varien.FileElement=Class.create();Varien.FileElement.prototype={initialize:function(b){this.fileElement=$(b);this.hiddenElement=$(b+"_value");this.fileElement.observe("change",this.selectFile.bind(this))},selectFile:function(b){this.hiddenElement.value=this.fileElement.getValue()}};Validation.addAllThese([["validate-custom"," ",function(b,d){return d.validate()}]]);Element.addMethods({getInnerText:function(b){b=$(b);if(b.innerText&&!Prototype.Browser.Opera){return b.innerText}return b.innerHTML.stripScripts().unescapeHTML().replace(/[\n\r\s]+/g," ").strip()}});function fireEvent(d,e){if(document.createEvent){var b=document.createEvent("HTMLEvents");b.initEvent(e,true,true);return d.dispatchEvent(b)}else{var b=document.createEventObject();return d.fireEvent("on"+e,b)}}function modulo(b,f){var e=f/10000;var d=b%f;if(Math.abs(d-f)b.toFixed(2).toString().length){b=b.toFixed(2)}return b+" "+d[e]};var SessionError=Class.create();SessionError.prototype={initialize:function(b){this.errorText=b},toString:function(){return"Session Error:"+this.errorText}};Ajax.Request.addMethods({initialize:function($super,d,b){$super(b);this.transport=Ajax.getTransport();if(!d.match(new RegExp("[?&]isAjax=true",""))){d=d.match(new RegExp("\\?","g"))?d+"&isAjax=true":d+"?isAjax=true"}if(Object.isString(this.options.parameters)&&this.options.parameters.indexOf("form_key=")==-1){this.options.parameters+="&"+Object.toQueryString({form_key:FORM_KEY})}else{if(!this.options.parameters){this.options.parameters={form_key:FORM_KEY}}if(!this.options.parameters.form_key){this.options.parameters.form_key=FORM_KEY}}this.request(d)},respondToReadyState:function(b){var g=Ajax.Request.Events[b],d=new Ajax.Response(this);if(g=="Complete"){try{this._complete=true;if(d.responseText.isJSON()){var f=d.responseText.evalJSON();if(f.ajaxExpired&&f.ajaxRedirect){window.location.replace(f.ajaxRedirect);throw new SessionError("session expired")}}(this.options["on"+d.status]||this.options["on"+(this.success()?"Success":"Failure")]||Prototype.emptyFunction)(d,d.headerJSON)}catch(h){this.dispatchException(h);if(h instanceof SessionError){return}}var l=d.getHeader("Content-type");if(this.options.evalJS=="force"||(this.options.evalJS&&this.isSameOrigin()&&l&&l.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))){this.evalResponse()}}try{(this.options["on"+g]||Prototype.emptyFunction)(d,d.headerJSON);Ajax.Responders.dispatch("on"+g,this,d,d.headerJSON)}catch(h){this.dispatchException(h)}if(g=="Complete"){this.transport.onreadystatechange=Prototype.emptyFunction}}});Ajax.Updater.respondToReadyState=Ajax.Request.respondToReadyState;var varienLoader=new Class.create();varienLoader.prototype={initialize:function(b){this.callback=false;this.cache=$H();this.caching=b||false;this.url=false},getCache:function(b){if(this.cache.get(b)){return this.cache.get(b)}return false},load:function(b,d,f){this.url=b;this.callback=f;if(this.caching){var e=this.getCache(b);if(e){this.processResult(e);return}}if(typeof(d.updaterId)!="undefined"){new varienUpdater(d.updaterId,b,{evalScripts:true,onComplete:this.processResult.bind(this),onFailure:this._processFailure.bind(this)})}else{new Ajax.Request(b,{method:"post",parameters:d||{},onComplete:this.processResult.bind(this),onFailure:this._processFailure.bind(this)})}},_processFailure:function(b){location.href=BASE_URL},processResult:function(b){if(this.caching){this.cache.set(this.url,b)}if(this.callback){this.callback(b.responseText)}}};if(!window.varienLoaderHandler){var varienLoaderHandler=new Object()}varienLoaderHandler.handler={onCreate:function(b){if(b.options.loaderArea===false){return}jQuery("body").trigger("processStart")},onException:function(b){jQuery("body").trigger("processStop")},onComplete:function(b){jQuery("body").trigger("processStop")}};function setLoaderPosition(){var e=$("loading_mask_loader");if(e&&Prototype.Browser.IE){var d=e.getDimensions();var f=document.viewport.getDimensions();var b=document.viewport.getScrollOffsets();e.style.left=Math.floor(f.width/2+b.left-d.width/2)+"px";e.style.top=Math.floor(f.height/2+b.top-d.height/2)+"px";e.style.position="absolute"}}function toggleSelectsUnderBlock(f,b){if(Prototype.Browser.IE){var e=document.getElementsByTagName("select");for(var d=0;d');d.document.close();Event.observe(d,"load",function(){var e=d.document.getElementById("image_preview");d.resizeTo(e.width+40,e.height+80)})}}function checkByProductPriceType(b){if(b.id=="price_type"){this.productPriceType=b.value;return false}else{if(b.id=="price"&&this.productPriceType==0){return false}return true}}Event.observe(window,"load",function(){if($("price_default")&&$("price_default").checked){$("price").disabled="disabled"}});function toggleSeveralValueElements(f,e,b,d){if(e&&f){if(Object.prototype.toString.call(e)!="[object Array]"){e=[e]}e.each(function(g){toggleValueElements(f,g,b,d)})}}function toggleValueElements(l,d,f,h){if(d&&l){var n=[l];if(typeof f!="undefined"){if(Object.prototype.toString.call(f)!="[object Array]"){f=[f]}for(var g=0;g>2;l=((p&3)<<4)|(n>>4);g=((n&15)<<2)|(h>>6);f=h&63;if(isNaN(n)){g=f=64}else{if(isNaN(h)){f=64}}b=b+this._keyStr.charAt(o)+this._keyStr.charAt(l)+this._keyStr.charAt(g)+this._keyStr.charAt(f)}return b},decode:function(e){var b="";var p,n,h;var o,l,g,f;var d=0;if(typeof window.atob==="function"){return Base64._utf8_decode(window.atob(e))}e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(d>4);n=((l&15)<<4)|(g>>2);h=((g&3)<<6)|f;b=b+String.fromCharCode(p);if(g!=64){b=b+String.fromCharCode(n)}if(f!=64){b=b+String.fromCharCode(h)}}return Base64._utf8_decode(b)},mageEncode:function(b){return this.encode(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,",")},mageDecode:function(b){b=b.replace(/\-/g,"+").replace(/_/g,"/").replace(/,/g,"=");return this.decode(b)},idEncode:function(b){return this.encode(b).replace(/\+/g,":").replace(/\//g,"_").replace(/=/g,"-")},idDecode:function(b){b=b.replace(/\-/g,"=").replace(/_/g,"/").replace(/\:/g,"+");return this.decode(b)},_utf8_encode:function(d){d=d.replace(/\r\n/g,"\n");var b="";for(var f=0;f127)&&(e<2048)){b+=String.fromCharCode((e>>6)|192);b+=String.fromCharCode((e&63)|128)}else{b+=String.fromCharCode((e>>12)|224);b+=String.fromCharCode(((e>>6)&63)|128);b+=String.fromCharCode((e&63)|128)}}}return b},_utf8_decode:function(b){var d="";var e=0;var f=c1=c2=0;while(e191)&&(f<224)){c2=b.charCodeAt(e+1);d+=String.fromCharCode(((f&31)<<6)|(c2&63));e+=2}else{c2=b.charCodeAt(e+1);c3=b.charCodeAt(e+2);d+=String.fromCharCode(((f&15)<<12)|((c2&63)<<6)|(c3&63));e+=3}}}return d}};function sortNumeric(d,b){return d-b}(function(){var globals=["Prototype","Abstract","Try","Class","PeriodicalExecuter","Template","$break","Enumerable","$A","$w","$H","Hash","$R","ObjectRange","Ajax","$","Form","Field","$F","Toggle","Insertion","$continue","Position","Windows","Dialog","array","WindowUtilities","Builder","Effect","validateCreditCard","Validator","Validation","removeDelimiters","parseNumber","popWin","setLocation","setPLocation","setLanguageCode","decorateGeneric","decorateTable","decorateList","decorateDataList","parseSidUrl","formatCurrency","expandDetails","isIE","Varien","fireEvent","modulo","byteConvert","SessionError","varienLoader","varienLoaderHandler","setLoaderPosition","toggleSelectsUnderBlock","varienUpdater","setElementDisable","toggleParentVis","toggleFieldsetVis","toggleVis","imagePreview","checkByProductPriceType","toggleSeveralValueElements","toggleValueElements","submitAndReloadArea","syncOnchangeValue","updateElementAtCursor","firebugEnabled","disableElement","enableElement","disableElements","enableElements","Cookie","Fieldset","Base64","sortNumeric","Element","$$","Sizzle","Selector","Window"];globals.forEach(function(prop){window[prop]=eval(prop)})})(); \ No newline at end of file diff --git a/lib/web/mage/adminhtml/tools.js b/lib/web/mage/adminhtml/tools.js index 9dcbb4df4831d..85e8134105f12 100644 --- a/lib/web/mage/adminhtml/tools.js +++ b/lib/web/mage/adminhtml/tools.js @@ -332,12 +332,12 @@ var Base64 = { var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; + input = Base64._utf8_encode(input); + if( typeof window.btoa === "function" ){ return window.btoa(input); } - input = Base64._utf8_encode(input); - while (i < input.length) { chr1 = input.charCodeAt(i++); @@ -370,7 +370,7 @@ var Base64 = { var i = 0; if( typeof window.atob === "function" ){ - return window.atob(input); + return Base64._utf8_decode(window.atob(input)); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); @@ -395,8 +395,8 @@ var Base64 = { output = output + String.fromCharCode(chr3); } } - output = Base64._utf8_decode(output); - return output; + + return Base64._utf8_decode(output); }, mageEncode: function(input){ From ba6612462c260da7cc534b6365623993a6fe4311 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 5 May 2017 09:08:31 +0300 Subject: [PATCH 038/363] MAGETWO-59690: [Backport] - [GitHub] Inserted image in product description got broken on front end #6138 - for 2.1 --- .../Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php | 6 ++++++ .../Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php | 5 ++++- lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php index 3c53eaaf1444e..dda3bb7831f42 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php @@ -14,6 +14,12 @@ use Magento\Backend\Block\Widget\Form; use Magento\Backend\Block\Widget\Form\Generic; +/** + * Class Content + * + * @deprecated + * @see \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav + */ class Content extends Generic { /** diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 176e578d7a73f..ce3dc1a97da91 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -745,7 +745,10 @@ private function customizeWysiwyg(ProductAttributeInterface $attribute, array $m $meta['arguments']['data']['config']['wysiwyg'] = true; $meta['arguments']['data']['config']['wysiwygConfigData'] = [ 'add_variables' => false, - 'add_widgets' => false + 'add_widgets' => false, + 'add_directives' => true, + 'use_container' => true, + 'container_class' => 'hor-scroll', ]; return $meta; diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js index 2e644cac0b451..5512f312daf0e 100755 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js @@ -343,6 +343,8 @@ define([ // escape special chars in directives url to use it in regular expression var url = this.makeDirectiveUrl('%directive%').replace(/([$^.?*!+:=()\[\]{}|\\])/g, '\\$1'); var reg = new RegExp(url.replace('%directive%', '([a-zA-Z0-9,_-]+)')); + content = decodeURIComponent(content); + return content.gsub(reg, function(match) { return Base64.mageDecode(match[1]); }.bind(this)); From e7bdb571b6489ceb2eeafc188872ac3cca068612 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Fri, 5 May 2017 10:19:18 +0300 Subject: [PATCH 039/363] MAGETWO-57051: [Backport] - [GitHub] UTF-8 special character issue in widgets #4232 - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-57051.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml deleted file mode 100644 index 18db002001ef6..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57051.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file From 34e325572fd899832923618eb9bba74242fcc60c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Fri, 5 May 2017 10:48:41 +0300 Subject: [PATCH 040/363] MAGETWO-63124: [Backport] - Incorrect scope filter caching in UI grids - for 2.1 --- .../AssertProductGridFilterCorrect.php | 4 ++++ .../Product/UpdateSimpleProductEntityTest.php | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php index 7acea4a50f695..de9c9940a0bb1 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductGridFilterCorrect.php @@ -11,6 +11,10 @@ use Magento\Mtf\Constraint\AbstractConstraint; use Magento\Mtf\Fixture\FixtureInterface; +/** + * Assert to check that name of product in grid is changing when store filter changed + * + */ class AssertProductGridFilterCorrect extends AbstractConstraint { /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php index ee72b6ab3f62d..a2d32d84afbe0 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit; use Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\TestStep\TestStepFactory; use Magento\Store\Test\Fixture\Store; /** @@ -57,19 +58,29 @@ class UpdateSimpleProductEntityTest extends Injectable */ protected $configData; + /** + * Test step factory. + * + * @var TestStepFactory + */ + private $testStepFactory; + /** * Injection data. * * @param CatalogProductIndex $productGrid * @param CatalogProductEdit $editProductPage + * @param TestStepFactory $testStepFactory * @return void */ public function __inject( CatalogProductIndex $productGrid, - CatalogProductEdit $editProductPage + CatalogProductEdit $editProductPage, + TestStepFactory $testStepFactory ) { $this->productGrid = $productGrid; $this->editProductPage = $editProductPage; + $this->testStepFactory = $testStepFactory; } /** @@ -103,7 +114,7 @@ public function test( $productName[$store->getStoreId()] = $product->getName(); } - $this->objectManager->create( + $this->testStepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $configData] )->run(); @@ -134,7 +145,7 @@ public function test( public function tearDown() { if ($this->configData) { - $this->objectManager->create( + $this->testStepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $this->configData, 'rollback' => true] )->run(); From cf13ae28a4076d4a6047d0365d89911ee88e9a7e Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 5 May 2017 11:04:06 +0300 Subject: [PATCH 041/363] MAGETWO-60777: [Backport] - [Magento Cloud] - Static files not being generated correctly on prod(Race condition in merging files) - for 2.1 --- .../Framework/Filesystem/Test/Unit/Directory/WriteTest.php | 3 +++ .../View/Test/Unit/Asset/MergeStrategy/DirectTest.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php index d972efae15f38..7661277cc8cf7 100644 --- a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php @@ -10,6 +10,9 @@ use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverInterface; +/** + * Write test. + */ class WriteTest extends \PHPUnit_Framework_TestCase { /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php index 553e7a0229008..c2b29fe1d31a2 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php @@ -9,6 +9,9 @@ use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\App\Filesystem\DirectoryList; +/** + * Direct merge strategy test. + */ class DirectTest extends \PHPUnit_Framework_TestCase { /** From 22b96e0704ba4a55a65acf81dbbd619d733a4d50 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 5 May 2017 13:02:27 +0300 Subject: [PATCH 042/363] MAGETWO-57060: [Backport] - Unable to apply free shipping to specified method when creating order in admin - for 2.1 --- .../Magento/Sales/Model/AdminOrder/Create.php | 62 +++++++----- .../Test/Unit/Model/AdminOrder/CreateTest.php | 96 ++++++++++++------- .../adminhtml/web/order/create/scripts.js | 2 +- .../Test/TestCase/CreateOrderBackendTest.xml | 17 ++++ .../SalesRule/Test/Handler/SalesRule/Curl.php | 28 +++--- .../SalesRule/Test/Repository/SalesRule.xml | 21 ++++ .../Magento/SalesRule/Test/etc/testcase.xml | 2 +- 7 files changed, 158 insertions(+), 70 deletions(-) diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index a97d486af4447..49d723f8906e8 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -608,7 +608,7 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q } $product = $this->_objectManager->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class )->setStoreId( $this->getSession()->getStoreId() )->load( @@ -662,7 +662,7 @@ public function getCustomerWishlist($cacheReload = false) $customerId = (int)$this->getSession()->getCustomerId(); if ($customerId) { - $this->_wishlist = $this->_objectManager->create('Magento\Wishlist\Model\Wishlist'); + $this->_wishlist = $this->_objectManager->create(\Magento\Wishlist\Model\Wishlist::class); $this->_wishlist->loadByCustomerId($customerId, true); $this->_wishlist->setStore( $this->getSession()->getStore() @@ -716,7 +716,9 @@ public function getCustomerCompareList() } $customerId = (int)$this->getSession()->getCustomerId(); if ($customerId) { - $this->_compareList = $this->_objectManager->create('Magento\Catalog\Model\Product\Compare\ListCompare'); + $this->_compareList = $this->_objectManager->create( + \Magento\Catalog\Model\Product\Compare\ListCompare::class + ); } else { $this->_compareList = false; } @@ -762,7 +764,7 @@ public function moveQuoteItem($item, $moveTo, $qty) $info->setOptions($this->_prepareOptionsForRequest($item))->setQty($qty); $product = $this->_objectManager->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class )->setStoreId( $this->getQuote()->getStoreId() )->load( @@ -784,7 +786,7 @@ public function moveQuoteItem($item, $moveTo, $qty) if ($cart && is_null($item->getOptionByCode('additional_options'))) { //options and info buy request $product = $this->_objectManager->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class )->setStoreId( $this->getQuote()->getStoreId() )->load( @@ -819,13 +821,17 @@ public function moveQuoteItem($item, $moveTo, $qty) $wishlist = null; if (!isset($moveTo[1])) { $wishlist = $this->_objectManager->create( - 'Magento\Wishlist\Model\Wishlist' + \Magento\Wishlist\Model\Wishlist::class )->loadByCustomerId( $this->getSession()->getCustomerId(), true ); } else { - $wishlist = $this->_objectManager->create('Magento\Wishlist\Model\Wishlist')->load($moveTo[1]); + $wishlist = $this->_objectManager->create( + \Magento\Wishlist\Model\Wishlist::class + ) + ->load($moveTo[1]); + if (!$wishlist->getId() || $wishlist->getCustomerId() != $this->getSession()->getCustomerId() ) { $wishlist = null; @@ -885,7 +891,7 @@ public function applySidebarData($data) if (isset($data['add_order_item'])) { foreach ($data['add_order_item'] as $orderItemId => $value) { /* @var $orderItem \Magento\Sales\Model\Order\Item */ - $orderItem = $this->_objectManager->create('Magento\Sales\Model\Order\Item')->load($orderItemId); + $orderItem = $this->_objectManager->create(\Magento\Sales\Model\Order\Item::class)->load($orderItemId); $item = $this->initFromOrderItem($orderItem); if (is_string($item)) { throw new \Magento\Framework\Exception\LocalizedException(__($item)); @@ -904,7 +910,7 @@ public function applySidebarData($data) if (isset($data['add_wishlist_item'])) { foreach ($data['add_wishlist_item'] as $itemId => $qty) { $item = $this->_objectManager->create( - 'Magento\Wishlist\Model\Item' + \Magento\Wishlist\Model\Item::class )->loadWithOptions( $itemId, 'info_buyRequest' @@ -957,12 +963,15 @@ public function removeItem($itemId, $from) case 'wishlist': $wishlist = $this->getCustomerWishlist(); if ($wishlist) { - $item = $this->_objectManager->create('Magento\Wishlist\Model\Item')->load($itemId); + $item = $this->_objectManager->create(\Magento\Wishlist\Model\Item::class)->load($itemId); $item->delete(); } break; case 'compared': - $this->_objectManager->create('Magento\Catalog\Model\Product\Compare\Item')->load($itemId)->delete(); + $this->_objectManager->create( + \Magento\Catalog\Model\Product\Compare\Item::class + ) + ->load($itemId)->delete(); break; } @@ -1003,7 +1012,7 @@ public function addProduct($product, $config = 1) if (!$product instanceof \Magento\Catalog\Model\Product) { $productId = $product; $product = $this->_objectManager->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class )->setStore( $this->getSession()->getStore() )->setStoreId( @@ -1103,7 +1112,7 @@ public function updateQuoteItems($items) protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additionalOptions) { $productOptions = $this->_objectManager->get( - 'Magento\Catalog\Model\Product\Option\Type\DefaultType' + \Magento\Catalog\Model\Product\Option\Type\DefaultType::class )->setProduct( $item->getProduct() )->getProductOptions(); @@ -1115,11 +1124,15 @@ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additio if (strlen(trim($_additionalOption))) { try { if (strpos($_additionalOption, ':') === false) { - throw new \Magento\Framework\Exception\LocalizedException(__('There is an error in one of the option rows.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('There is an error in one of the option rows.') + ); } list($label, $value) = explode(':', $_additionalOption, 2); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('There is an error in one of the option rows.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('There is an error in one of the option rows.') + ); } $label = trim($label); $value = trim($value); @@ -1132,7 +1145,7 @@ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additio $option = $item->getProduct()->getOptionById($optionId); $group = $this->_objectManager->get( - 'Magento\Catalog\Model\Product\Option' + \Magento\Catalog\Model\Product\Option::class )->groupFactory( $option->getType() )->setOption( @@ -1231,7 +1244,7 @@ protected function _prepareOptionsForRequest($item) $optionValue = $item->getOptionByCode('option_' . $optionId)->getValue(); $group = $this->_objectManager->get( - 'Magento\Catalog\Model\Product\Option' + \Magento\Catalog\Model\Product\Option::class )->groupFactory( $option->getType() )->setOption( @@ -1255,7 +1268,7 @@ protected function _prepareOptionsForRequest($item) */ protected function _parseCustomPrice($price) { - $price = $this->_objectManager->get('Magento\Framework\Locale\FormatInterface')->getNumber($price); + $price = $this->_objectManager->get(\Magento\Framework\Locale\FormatInterface::class)->getNumber($price); $price = $price > 0 ? $price : 0; return $price; @@ -1360,7 +1373,7 @@ public function setShippingAddress($address) { if (is_array($address)) { $shippingAddress = $this->_objectManager->create( - 'Magento\Quote\Model\Quote\Address' + \Magento\Quote\Model\Quote\Address::class )->setData( $address )->setAddressType( @@ -1428,7 +1441,7 @@ public function setBillingAddress($address) { if (is_array($address)) { $billingAddress = $this->_objectManager->create( - 'Magento\Quote\Model\Quote\Address' + \Magento\Quote\Model\Quote\Address::class )->setData( $address )->setAddressType( @@ -1543,6 +1556,11 @@ public function setPaymentData($data) public function applyCoupon($code) { $code = trim((string)$code); + $this->getQuote()->getShippingAddress()->setCollectShippingRates(true); + + if (empty($code)) { + $this->getQuote()->getShippingAddress()->setFreeShipping(null); + } $this->getQuote()->setCouponCode($code); $this->setRecollect(true); @@ -1568,7 +1586,7 @@ public function setAccountData($accountData) $this->dataObjectHelper->populateWithArray( $customer, $data, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $this->getQuote()->updateCustomerData($customer); $data = []; @@ -1690,7 +1708,7 @@ protected function _validateCustomerData(\Magento\Customer\Api\Data\CustomerInte $this->dataObjectHelper->populateWithArray( $customer, $data, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); return $customer; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php index 007c529439e41..fce267d48f283 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php @@ -92,98 +92,98 @@ class CreateTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $objectManagerMock = $this->getMock('Magento\Framework\ObjectManagerInterface'); - $eventManagerMock = $this->getMock('Magento\Framework\Event\ManagerInterface'); - $registryMock = $this->getMock('Magento\Framework\Registry'); - $configMock = $this->getMock('Magento\Sales\Model\Config', [], [], '', false); - $this->sessionQuoteMock = $this->getMock('Magento\Backend\Model\Session\Quote', [], [], '', false); - $loggerMock = $this->getMock('Psr\Log\LoggerInterface'); - $copyMock = $this->getMock('Magento\Framework\DataObject\Copy', [], [], '', false); - $messageManagerMock = $this->getMock('Magento\Framework\Message\ManagerInterface'); + $objectManagerMock = $this->getMock(\Magento\Framework\ObjectManagerInterface::class); + $eventManagerMock = $this->getMock(\Magento\Framework\Event\ManagerInterface::class); + $registryMock = $this->getMock(\Magento\Framework\Registry::class); + $configMock = $this->getMock(\Magento\Sales\Model\Config::class, [], [], '', false); + $this->sessionQuoteMock = $this->getMock(\Magento\Backend\Model\Session\Quote::class, [], [], '', false); + $loggerMock = $this->getMock(\Psr\Log\LoggerInterface::class); + $copyMock = $this->getMock(\Magento\Framework\DataObject\Copy::class, [], [], '', false); + $messageManagerMock = $this->getMock(\Magento\Framework\Message\ManagerInterface::class); $this->formFactoryMock = $this->getMock( - 'Magento\Customer\Model\Metadata\FormFactory', + \Magento\Customer\Model\Metadata\FormFactory::class, ['create'], [], '', false ); $this->customerFactoryMock = $this->getMock( - 'Magento\Customer\Api\Data\CustomerInterfaceFactory', + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create'], [], '', false ); - $this->itemUpdater = $this->getMock('Magento\Quote\Model\Quote\Item\Updater', [], [], '', false); + $this->itemUpdater = $this->getMock(\Magento\Quote\Model\Quote\Item\Updater::class, [], [], '', false); - $this->objectFactory = $this->getMockBuilder('\Magento\Framework\DataObject\Factory') + $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); $this->customerMapper = $this->getMockBuilder( - 'Magento\Customer\Model\Customer\Mapper' + \Magento\Customer\Model\Customer\Mapper::class )->setMethods(['toFlatArray'])->disableOriginalConstructor()->getMock(); $this->quoteInitializerMock = $this->getMock( - 'Magento\Sales\Model\AdminOrder\Product\Quote\Initializer', + \Magento\Sales\Model\AdminOrder\Product\Quote\Initializer::class, [], [], '', false ); $this->customerRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\CustomerRepositoryInterface', + \Magento\Customer\Api\CustomerRepositoryInterface::class, [], '', false ); $this->addressRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\AddressRepositoryInterface', + \Magento\Customer\Api\AddressRepositoryInterface::class, [], '', false ); $this->addressFactoryMock = $this->getMock( - 'Magento\Customer\Api\Data\AddressInterfaceFactory', + \Magento\Customer\Api\Data\AddressInterfaceFactory::class, [], [], '', false ); $this->groupRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\GroupRepositoryInterface', + \Magento\Customer\Api\GroupRepositoryInterface::class, [], '', false ); $this->scopeConfigMock = $this->getMockForAbstractClass( - 'Magento\Framework\App\Config\ScopeConfigInterface', + \Magento\Framework\App\Config\ScopeConfigInterface::class, [], '', false ); $this->emailSenderMock = $this->getMock( - 'Magento\Sales\Model\AdminOrder\EmailSender', + \Magento\Sales\Model\AdminOrder\EmailSender::class, [], [], '', false ); $this->accountManagementMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\AccountManagementInterface', + \Magento\Customer\Api\AccountManagementInterface::class, [], '', false ); - $this->dataObjectHelper = $this->getMockBuilder('Magento\Framework\Api\DataObjectHelper') + $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) ->disableOriginalConstructor() ->getMock(); $objectManagerHelper = new ObjectManagerHelper($this); $this->adminOrderCreate = $objectManagerHelper->getObject( - 'Magento\Sales\Model\AdminOrder\Create', + \Magento\Sales\Model\AdminOrder\Create::class, [ 'objectManager' => $objectManagerMock, 'eventManager' => $eventManagerMock, @@ -220,7 +220,7 @@ public function testSetAccountData() foreach ($attributes as $attribute) { $attributeMock = $this->getMock( - 'Magento\Customer\Api\Data\AttributeMetadataInterface', + \Magento\Customer\Api\Data\AttributeMetadataInterface::class, [], [], '', @@ -233,7 +233,7 @@ public function testSetAccountData() } $customerGroupMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\GroupInterface', + \Magento\Customer\Api\Data\GroupInterface::class, [], '', false, @@ -242,7 +242,7 @@ public function testSetAccountData() ['getTaxClassId'] ); $customerGroupMock->expects($this->once())->method('getTaxClassId')->will($this->returnValue($taxClassId)); - $customerFormMock = $this->getMock('Magento\Customer\Model\Metadata\Form', [], [], '', false); + $customerFormMock = $this->getMock(\Magento\Customer\Model\Metadata\Form::class, [], [], '', false); $customerFormMock->expects($this->any()) ->method('getAttributes') ->will($this->returnValue([$attributeMocks[1]])); @@ -251,15 +251,15 @@ public function testSetAccountData() $customerFormMock->expects($this->any()) ->method('prepareRequest') - ->will($this->returnValue($this->getMock('Magento\Framework\App\RequestInterface'))); + ->will($this->returnValue($this->getMock(\Magento\Framework\App\RequestInterface::class))); - $customerMock = $this->getMock('Magento\Customer\Api\Data\CustomerInterface', [], [], '', false); + $customerMock = $this->getMock(\Magento\Customer\Api\Data\CustomerInterface::class, [], [], '', false); $this->customerMapper->expects($this->atLeastOnce()) ->method('toFlatArray') ->willReturn(['group_id' => 1]); - $quoteMock = $this->getMock('Magento\Quote\Model\Quote', [], [], '', false); + $quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); $quoteMock->expects($this->any())->method('getCustomer')->will($this->returnValue($customerMock)); $quoteMock->expects($this->once()) ->method('addData') @@ -274,7 +274,7 @@ public function testSetAccountData() ->with( $customerMock, ['group_id' => 1], - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $this->formFactoryMock->expects($this->any())->method('create')->will($this->returnValue($customerFormMock)); @@ -303,9 +303,9 @@ public function testUpdateQuoteItemsEmptyConfiguredOption() ] ]; - $itemMock = $this->getMock('Magento\Quote\Model\Quote\Item', [], [], '', false); + $itemMock = $this->getMock(\Magento\Quote\Model\Quote\Item::class, [], [], '', false); - $quoteMock = $this->getMock('Magento\Quote\Model\Quote', [], [], '', false); + $quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); $quoteMock->expects($this->once()) ->method('getItemById') ->will($this->returnValue($itemMock)); @@ -331,12 +331,12 @@ public function testUpdateQuoteItemsWithConfiguredOption() ] ]; - $itemMock = $this->getMock('Magento\Quote\Model\Quote\Item', [], [], '', false); + $itemMock = $this->getMock(\Magento\Quote\Model\Quote\Item::class, [], [], '', false); $itemMock->expects($this->once()) ->method('getQty') ->will($this->returnValue($qty)); - $quoteMock = $this->getMock('Magento\Quote\Model\Quote', [], [], '', false); + $quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); $quoteMock->expects($this->once()) ->method('updateItem') ->will($this->returnValue($itemMock)); @@ -352,4 +352,32 @@ public function testUpdateQuoteItemsWithConfiguredOption() $this->adminOrderCreate->setRecollect(false); $this->adminOrderCreate->updateQuoteItems($items); } + + public function testApplyCoupon() + { + $couponCode = ''; + $quoteMock = $this->getMock( + \Magento\Quote\Model\Quote::class, + ['getShippingAddress', 'setCouponCode'], + [], + '', + false + ); + $this->sessionQuoteMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); + + $addressMock = $this->getMock( + \Magento\Quote\Model\Quote\Address::class, + ['setCollectShippingRates', 'setFreeShipping'], + [], + '', + false + ); + $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($addressMock); + $quoteMock->expects($this->once())->method('setCouponCode')->with($couponCode)->willReturnSelf(); + + $addressMock->expects($this->once())->method('setCollectShippingRates')->with(true)->willReturnSelf(); + $addressMock->expects($this->once())->method('setFreeShipping')->with(null)->willReturnSelf(); + + $this->adminOrderCreate->applyCoupon($couponCode); + } } diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index f30568215befd..673fba9792dfa 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -434,7 +434,7 @@ define([ }, applyCoupon : function(code){ - this.loadArea(['items', 'shipping_method', 'totals', 'billing_method'], true, {'order[coupon][code]':code, reset_shipping: true}); + this.loadArea(['items', 'shipping_method', 'totals', 'billing_method'], true, {'order[coupon][code]':code, reset_shipping: 0}); this.orderItemChanged = false; }, diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml index 18be3914847a3..90c65091c117e 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml @@ -148,5 +148,22 @@ + + test_type:acceptance_test + catalogProductSimple::default + active_sales_rule_free_shipping + default + Yes + register + US_address_1_without_email + Flat Rate + Fixed + + 560.00 + + checkmo + + + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php index 9b8aa37304e04..890ab1695f0d5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php @@ -38,52 +38,56 @@ class Curl extends Conditions implements SalesRuleInterface */ protected $mapTypeParams = [ 'Subtotal' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'base_subtotal', ], 'Total Items Quantity' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'total_qty', ], 'Conditions combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Combine', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, 'aggregator' => 'all', 'value' => '1', ], 'Products subselection' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Subselect', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'aggregator' => 'all', ], 'Product attribute combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Found', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, 'value' => '1', 'aggregator' => 'all', ], 'Shipping Country' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'country_id', ], 'Shipping Postcode' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'postcode', ], + 'Shipping Method' => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'shipping_method', + ], 'Category' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'category_ids', ], 'Price in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_price', ], 'Quantity in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_qty', ], 'Row total in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_row_total', ] ]; @@ -207,7 +211,7 @@ public function prepareData(FixtureInterface $fixture) if (isset($this->data['actions_serialized'])) { $this->mapTypeParams['Conditions combination']['type'] = - 'Magento\SalesRule\Model\Rule\Condition\Product\Combine'; + \Magento\SalesRule\Model\Rule\Condition\Product\Combine::class; $this->data['rule']['actions'] = $this->prepareCondition($this->data['actions_serialized']); unset($this->data['actions_serialized']); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 13479dbfc19e4..b904d2dba10b5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -279,5 +279,26 @@ No No + + + Active Sales Rule Free Shipping %isolation% + Yes + + Main Website + + + NOT LOGGED IN + General + Wholesale + Retailer + + Specific Coupon + free-shipping-%isolation% + Percent of product price discount + 0 + Yes + For matching items only + [Shipping Method|is|flatrate_flatrate] + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/etc/testcase.xml index 21648100eacc1..ed13b228afca4 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/etc/testcase.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/etc/testcase.xml @@ -33,7 +33,7 @@ - + From ae74de0982bfb86bc300d188768823f7eb7e7454 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 5 May 2017 13:05:37 +0300 Subject: [PATCH 043/363] MAGETWO-57060: [Backport] - Unable to apply free shipping to specified method when creating order in admin - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-57060.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml new file mode 100644 index 0000000000000..fb801a30d15f1 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml @@ -0,0 +1,14 @@ + + + + + + + + + From c6ef72a018055f6eabe576ae0e97dc23a467c9c1 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Fri, 5 May 2017 13:25:22 +0300 Subject: [PATCH 044/363] MAGETWO-62914: [Backport] - [Github] Directive values are not quote-escaped #3860 - for 2.1 --- .../Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php index 2f78208bb6c92..b13bbdc55b884 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php @@ -7,6 +7,9 @@ use Magento\Framework\App\ObjectManager; +/** + * Loading widget options + */ class LoadOptions extends \Magento\Backend\App\Action { /** From 9b2a49dc896191050bf961ab4fdfd75fc8afb172 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 5 May 2017 14:45:51 +0300 Subject: [PATCH 045/363] MAGETWO-57060: [Backport] - Unable to apply free shipping to specified method when creating order in admin - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-57060.xml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml deleted file mode 100644 index fb801a30d15f1..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57060.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - From 44e719b9ecd5871cf5ed62e6ce3c34d84c9fd9ae Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 5 May 2017 15:20:25 +0300 Subject: [PATCH 046/363] MAGETWO-60742: TEST ISSUE: Failed Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest - Variation3 --- .../InjectableTests/magetwo_60742.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml new file mode 100644 index 0000000000000..8e6dcdecdfdb7 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file From 34efac586191eeb858ce037abdffd33d5d2fba28 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 5 May 2017 15:27:08 +0300 Subject: [PATCH 047/363] MAGETWO-60742: TEST ISSUE: Failed Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest - Variation3 --- .../Test/TestCase/CreateConfigurableProductEntityTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml index 86cd87ffac1bf..2e59228973898 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml @@ -54,7 +54,7 @@ - to_maintain:no + to_maintain:no, test_type:MAGETWO-60742 configurable-product-%isolation% two_options_with_assigned_product_special_price configurable_two_new_options_with_special_price From 44da7a57d33e5ba784bf4b5efe44ea77c4e35076 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 5 May 2017 15:47:31 +0300 Subject: [PATCH 048/363] MAGETWO-60742: TEST ISSUE: Failed Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest - Variation3 --- .../CreateConfigurableProductEntityTest.xml | 2 +- .../InjectableTests/magetwo_60742.xml | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml index 2e59228973898..86cd87ffac1bf 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml @@ -54,7 +54,7 @@ - to_maintain:no, test_type:MAGETWO-60742 + to_maintain:no configurable-product-%isolation% two_options_with_assigned_product_special_price configurable_two_new_options_with_special_price diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml deleted file mode 100644 index 8e6dcdecdfdb7..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/magetwo_60742.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file From bb3d06228dfb00e250c2b44e4d80465d1a3e1ad5 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Fri, 5 May 2017 16:44:13 +0300 Subject: [PATCH 049/363] MAGETWO-56937: [Backport] - [GitHub] Setting SKU mask as empty crashes product Javascript in backend #5618 - for 2.1 --- .../web/js/components/import-handler.js | 20 +++++++++---------- .../Catalog/Test/Repository/ConfigData.xml | 11 ++++++++++ .../Product/CreateSimpleProductEntityTest.xml | 17 ++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js index 3d3c30e4797ae..a6c71e7d4b660 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js @@ -4,8 +4,9 @@ */ define([ + 'underscore', 'Magento_Ui/js/form/element/textarea' -], function (Textarea) { +], function (_, Textarea) { 'use strict'; return Textarea.extend({ @@ -123,24 +124,21 @@ define([ * Update field value, if it's allowed */ updateValue: function () { - var str = this.mask, + var str = this.mask || '', nonEmptyValueFlag = false, - placeholder, - property, tmpElement; if (!this.allowImport) { return; } - for (property in this.values) { - if (this.values.hasOwnProperty(property)) { - placeholder = ''; - placeholder = placeholder.concat('{{', property, '}}'); - str = str.replace(placeholder, this.values[property]); - nonEmptyValueFlag = nonEmptyValueFlag || !!this.values[property]; - } + if (str) { + _.each(this.values, function (propertyValue, propertyName) { + str = str.replace('{{' + propertyName + '}}', propertyValue); + nonEmptyValueFlag = nonEmptyValueFlag || !!propertyValue; + }); } + // strip tags tmpElement = document.createElement('div'); tmpElement.innerHTML = str; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml index 093dc6043cdc1..e4516d83075f3 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml @@ -23,5 +23,16 @@ 0
+ + + + + + + + {{name}} + 1 + + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index 8bd0f98b54bcc..4650c2057546e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -449,5 +449,22 @@ + + empty_product_mask_sku + Create product with custom options(fixed price) + simple-product-%isolation% + Simple Product %isolation% + simple_sku_%isolation% + 10000 + 50 + 657 + simple_drop_down_with_one_option_fixed_price + drop_down_with_one_option_fixed_price + + + + + + From 0cee64b4e072a71ab789825982567df5cbf2195f Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Mon, 8 May 2017 11:24:31 +0300 Subject: [PATCH 050/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../ResourceModel/CustomerRepository.php | 31 ++++- .../ResourceModel/CustomerRepositoryTest.php | 106 ++++++++++-------- .../ResourceModel/CustomerRepositoryTest.php | 98 ++++++++++++---- 3 files changed, 163 insertions(+), 72 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index a996b059455eb..7a1c6c0bbd46b 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -15,6 +15,7 @@ /** * Customer repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface { @@ -137,9 +138,15 @@ public function __construct( public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) { $prevCustomerData = null; + $prevCustomerDataArr = null; + if ($customer->getId()) { $prevCustomerData = $this->getById($customer->getId()); + $prevCustomerDataArr = $prevCustomerData->__toArray(); } + + /** @var $customer \Magento\Customer\Model\Data\Customer */ + $customerArr = $customer->__toArray(); $customer = $this->imageProcessor->save( $customer, CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -151,15 +158,17 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $customerData = $this->extensibleDataObjectConverter->toNestedArray( $customer, [], - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $customer->setAddresses($origAddresses); $customerModel = $this->customerFactory->create(['data' => $customerData]); $storeId = $customerModel->getStoreId(); + if ($storeId === null) { $customerModel->setStoreId($this->storeManager->getStore()->getId()); } + $customerModel->setId($customer->getId()); // Need to use attribute set or future updates can cause data loss @@ -190,6 +199,21 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $customerModel->setRpToken(null); $customerModel->setRpTokenCreatedAt(null); } + + if (!array_key_exists('default_billing', $customerArr) && + null !== $prevCustomerDataArr && + array_key_exists('default_billing', $prevCustomerDataArr) + ) { + $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); + } + + if (!array_key_exists('default_shipping', $customerArr) && + null !== $prevCustomerDataArr && + array_key_exists('default_shipping', $prevCustomerDataArr) + ) { + $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); + } + $customerModel->save(); $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); @@ -256,7 +280,10 @@ public function getList(SearchCriteriaInterface $searchCriteria) $searchResults->setSearchCriteria($searchCriteria); /** @var \Magento\Customer\Model\ResourceModel\Customer\Collection $collection */ $collection = $this->customerFactory->create()->getCollection(); - $this->extensionAttributesJoinProcessor->process($collection, 'Magento\Customer\Api\Data\CustomerInterface'); + $this->extensionAttributesJoinProcessor->process( + $collection, + \Magento\Customer\Api\Data\CustomerInterface::class + ); // This is needed to make sure all the attributes are properly loaded foreach ($this->customerMetadata->getAllAttributesMetadata() as $metadata) { $collection->addAttributeToSelect($metadata->getAttributeCode()); diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 0152b8631051e..0178d2d9c86b6 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -91,77 +91,78 @@ class CustomerRepositoryTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->customerResourceModel = - $this->getMock('Magento\Customer\Model\ResourceModel\Customer', [], [], '', false); - $this->customerRegistry = $this->getMock('Magento\Customer\Model\CustomerRegistry', [], [], '', false); - $this->dataObjectHelper = $this->getMock('Magento\Framework\Api\DataObjectHelper', [], [], '', false); - $this->customerFactory = $this->getMock('Magento\Customer\Model\CustomerFactory', ['create'], [], '', false); + $this->getMock(\Magento\Customer\Model\ResourceModel\Customer::class, [], [], '', false); + $this->customerRegistry = $this->getMock(\Magento\Customer\Model\CustomerRegistry::class, [], [], '', false); + $this->dataObjectHelper = $this->getMock(\Magento\Framework\Api\DataObjectHelper::class, [], [], '', false); + $this->customerFactory = $this->getMock( + \Magento\Customer\Model\CustomerFactory::class, + ['create'], + [], + '', + false + ); $this->customerSecureFactory = $this->getMock( - 'Magento\Customer\Model\Data\CustomerSecureFactory', + \Magento\Customer\Model\Data\CustomerSecureFactory::class, ['create'], [], '', false ); - $this->addressRepository = $this->getMock( - 'Magento\Customer\Model\ResourceModel\AddressRepository', + \Magento\Customer\Model\ResourceModel\AddressRepository::class, [], [], '', false ); - $this->customerMetadata = $this->getMockForAbstractClass( - 'Magento\Customer\Api\CustomerMetadataInterface', + \Magento\Customer\Api\CustomerMetadataInterface::class, [], '', false ); $this->searchResultsFactory = $this->getMock( - 'Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory', + \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory::class, ['create'], [], '', false ); $this->eventManager = $this->getMockForAbstractClass( - 'Magento\Framework\Event\ManagerInterface', + \Magento\Framework\Event\ManagerInterface::class, [], '', false ); $this->storeManager = $this->getMockForAbstractClass( - 'Magento\Store\Model\StoreManagerInterface', + \Magento\Store\Model\StoreManagerInterface::class, [], '', false ); $this->extensibleDataObjectConverter = $this->getMock( - 'Magento\Framework\Api\ExtensibleDataObjectConverter', + \Magento\Framework\Api\ExtensibleDataObjectConverter::class, [], [], '', false ); $this->imageProcessor = $this->getMockForAbstractClass( - 'Magento\Framework\Api\ImageProcessorInterface', + \Magento\Framework\Api\ImageProcessorInterface::class, [], '', false ); $this->extensionAttributesJoinProcessor = $this->getMockForAbstractClass( - 'Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface', - [], - '', - false - ); - $this->customer = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\CustomerInterface', + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class, [], '', false ); - + $this->customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['__toArray']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->model = new \Magento\Customer\Model\ResourceModel\CustomerRepository( $this->customerFactory, $this->customerSecureFactory, @@ -187,9 +188,9 @@ public function testSave() $customerId = 1; $storeId = 2; - $region = $this->getMockForAbstractClass('Magento\Customer\Api\Data\RegionInterface', [], '', false); + $region = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\RegionInterface::class, [], '', false); $address = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressInterface', + \Magento\Customer\Api\Data\AddressInterface::class, [], '', false, @@ -203,7 +204,7 @@ public function testSave() ] ); $address2 = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressInterface', + \Magento\Customer\Api\Data\AddressInterface::class, [], '', false, @@ -217,7 +218,7 @@ public function testSave() ] ); $customerModel = $this->getMock( - 'Magento\Customer\Model\Customer', + \Magento\Customer\Model\Customer::class, [ 'getId', 'setId', @@ -238,8 +239,11 @@ public function testSave() '', false ); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn(['default_billing', 'default_shipping']); $customerAttributesMetaData = $this->getMockForAbstractClass( - 'Magento\Framework\Api\CustomAttributesDataInterface', + \Magento\Framework\Api\CustomAttributesDataInterface::class, [], '', false, @@ -254,7 +258,7 @@ public function testSave() ] ); $customerSecureData = $this->getMock( - 'Magento\Customer\Model\Data\CustomerSecure', + \Magento\Customer\Model\Data\CustomerSecure::class, [ 'getRpToken', 'getRpTokenCreatedAt', @@ -305,7 +309,7 @@ public function testSave() ->with([$address]); $this->extensibleDataObjectConverter->expects($this->once()) ->method('toNestedArray') - ->with($customerAttributesMetaData, [], '\Magento\Customer\Api\Data\CustomerInterface') + ->with($customerAttributesMetaData, [], \Magento\Customer\Api\Data\CustomerInterface::class) ->willReturn(['customerData']); $this->customerFactory->expects($this->once()) ->method('create') @@ -428,9 +432,9 @@ public function testSaveWithPasswordHash() $storeId = 2; $passwordHash = 'ukfa4sdfa56s5df02asdf4rt'; - $region = $this->getMockForAbstractClass('Magento\Customer\Api\Data\RegionInterface', [], '', false); + $region = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\RegionInterface::class, [], '', false); $address = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressInterface', + \Magento\Customer\Api\Data\AddressInterface::class, [], '', false, @@ -444,7 +448,7 @@ public function testSaveWithPasswordHash() ] ); $address2 = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressInterface', + \Magento\Customer\Api\Data\AddressInterface::class, [], '', false, @@ -457,8 +461,12 @@ public function testSaveWithPasswordHash() 'getId' ] ); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn(['default_billing', 'default_shipping']); + $customerModel = $this->getMock( - 'Magento\Customer\Model\Customer', + \Magento\Customer\Model\Customer::class, [ 'getId', 'setId', @@ -477,7 +485,7 @@ public function testSaveWithPasswordHash() false ); $customerAttributesMetaData = $this->getMockForAbstractClass( - 'Magento\Framework\Api\CustomAttributesDataInterface', + \Magento\Framework\Api\CustomAttributesDataInterface::class, [], '', false, @@ -529,7 +537,7 @@ public function testSaveWithPasswordHash() ->with([$address]); $this->extensibleDataObjectConverter->expects($this->once()) ->method('toNestedArray') - ->with($customerAttributesMetaData, [], '\Magento\Customer\Api\Data\CustomerInterface') + ->with($customerAttributesMetaData, [], \Magento\Customer\Api\Data\CustomerInterface::class) ->willReturn(['customerData']); $this->customerFactory->expects($this->once()) ->method('create') @@ -600,24 +608,30 @@ public function testSaveWithPasswordHash() */ public function testGetList() { - $sortOrder = $this->getMock('Magento\Framework\Api\SortOrder', [], [], '', false); - $filterGroup = $this->getMock('Magento\Framework\Api\Search\FilterGroup', [], [], '', false); - $filter = $this->getMock('Magento\Framework\Api\Filter', [], [], '', false); - $collection = $this->getMock('Magento\Customer\Model\ResourceModel\Customer\Collection', [], [], '', false); + $sortOrder = $this->getMock(\Magento\Framework\Api\SortOrder::class, [], [], '', false); + $filterGroup = $this->getMock(\Magento\Framework\Api\Search\FilterGroup::class, [], [], '', false); + $filter = $this->getMock(\Magento\Framework\Api\Filter::class, [], [], '', false); + $collection = $this->getMock( + \Magento\Customer\Model\ResourceModel\Customer\Collection::class, + [], + [], + '', + false + ); $searchResults = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressSearchResultsInterface', + \Magento\Customer\Api\Data\AddressSearchResultsInterface::class, [], '', false ); $searchCriteria = $this->getMockForAbstractClass( - 'Magento\Framework\Api\SearchCriteriaInterface', + \Magento\Framework\Api\SearchCriteriaInterface::class, [], '', false ); $customerModel = $this->getMock( - 'Magento\Customer\Model\Customer', + \Magento\Customer\Model\Customer::class, [ 'getId', 'setId', @@ -636,7 +650,7 @@ public function testGetList() false ); $metadata = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AttributeMetadataInterface', + \Magento\Customer\Api\Data\AttributeMetadataInterface::class, [], '', false @@ -656,7 +670,7 @@ public function testGetList() ->willReturn($collection); $this->extensionAttributesJoinProcessor->expects($this->once()) ->method('process') - ->with($collection, 'Magento\Customer\Api\Data\CustomerInterface'); + ->with($collection, \Magento\Customer\Api\Data\CustomerInterface::class); $this->customerMetadata->expects($this->once()) ->method('getAllAttributesMetadata') ->willReturn([$metadata]); @@ -757,7 +771,7 @@ public function testDeleteById() { $customerId = 14; $customerModel = $this->getMock( - 'Magento\Customer\Model\Customer', + \Magento\Customer\Model\Customer::class, ['delete'], [], '', @@ -781,7 +795,7 @@ public function testDelete() { $customerId = 14; $customerModel = $this->getMock( - 'Magento\Customer\Model\Customer', + \Magento\Customer\Model\Customer::class, ['delete'], [], '', diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 14e98eab34b95..3406de3d47eaf 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -13,6 +13,8 @@ /** * Checks Customer insert, update, search with repository + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomerRepositoryTest extends \PHPUnit_Framework_TestCase { @@ -43,15 +45,25 @@ class CustomerRepositoryTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->customerRepository = $this->objectManager->create('Magento\Customer\Api\CustomerRepositoryInterface'); - $this->customerFactory = $this->objectManager->create('Magento\Customer\Api\Data\CustomerInterfaceFactory'); - $this->addressFactory = $this->objectManager->create('Magento\Customer\Api\Data\AddressInterfaceFactory'); - $this->regionFactory = $this->objectManager->create('Magento\Customer\Api\Data\RegionInterfaceFactory'); - $this->accountManagement = $this->objectManager->create('Magento\Customer\Api\AccountManagementInterface'); - $this->converter = $this->objectManager->create('Magento\Framework\Api\ExtensibleDataObjectConverter'); - $this->dataObjectHelper = $this->objectManager->create('Magento\Framework\Api\DataObjectHelper'); + $this->customerRepository = $this->objectManager->create( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->customerFactory = $this->objectManager->create( + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class + ); + $this->addressFactory = $this->objectManager->create( + \Magento\Customer\Api\Data\AddressInterfaceFactory::class + ); + $this->regionFactory = $this->objectManager->create( + \Magento\Customer\Api\Data\RegionInterfaceFactory::class + ); + $this->accountManagement = $this->objectManager->create( + \Magento\Customer\Api\AccountManagementInterface::class + ); + $this->converter = $this->objectManager->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class); + $this->dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); /** @var \Magento\Framework\Config\CacheInterface $cache */ - $cache = $this->objectManager->create('Magento\Framework\Config\CacheInterface'); + $cache = $this->objectManager->create(\Magento\Framework\Config\CacheInterface::class); $cache->remove('extension_attributes_config'); } @@ -59,7 +71,7 @@ protected function tearDown() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ - $customerRegistry = $objectManager->get('Magento\Customer\Model\CustomerRegistry'); + $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); $customerRegistry->remove(1); } @@ -85,7 +97,7 @@ public function testCreateCustomerNewThenUpdateFirstName() $newCustomerFirstname = 'New First Name'; $updatedCustomer = $this->customerFactory->create(); $this->dataObjectHelper->mergeDataObjects( - '\Magento\Customer\Api\Data\CustomerInterface', + \Magento\Customer\Api\Data\CustomerInterface::class, $updatedCustomer, $customer ); @@ -152,7 +164,7 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) $this->dataObjectHelper->populateWithArray( $customerDetails, $customerData, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $this->customerRepository->save($customerDetails); $customerAfter = $this->customerRepository->getById($existingCustomerId); @@ -172,12 +184,12 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) $attributesBefore = $this->converter->toFlatArray( $customerBefore, [], - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $attributesAfter = $this->converter->toFlatArray( $customerAfter, [], - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); // ignore 'updated_at' unset($attributesBefore['updated_at']); @@ -218,14 +230,14 @@ public function testUpdateCustomerAddress() $this->dataObjectHelper->populateWithArray( $newAddressDataObject, $newAddress, - '\Magento\Customer\Api\Data\AddressInterface' + \Magento\Customer\Api\Data\AddressInterface::class ); $newAddressDataObject->setRegion($addresses[0]->getRegion()); $newCustomerEntity = $this->customerFactory->create(); $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $newCustomerEntity->setId($customerId) ->setAddresses([$newAddressDataObject, $addresses[1]]); @@ -254,7 +266,7 @@ public function testUpdateCustomerPreserveAllAddresses() $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $newCustomerEntity->setId($customer->getId()) ->setAddresses(null); @@ -279,7 +291,7 @@ public function testUpdateCustomerDeleteAllAddressesWithEmptyArray() $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $newCustomerEntity->setId($customer->getId()) ->setAddresses([]); @@ -304,7 +316,7 @@ public function testSearchCustomers($filters, $filterGroup, $expectedResult) { /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ $searchBuilder = Bootstrap::getObjectManager() - ->create('Magento\Framework\Api\SearchCriteriaBuilder'); + ->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); foreach ($filters as $filter) { $searchBuilder->addFilters([$filter]); } @@ -333,17 +345,17 @@ public function testSearchCustomersOrder() { /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ $objectManager = Bootstrap::getObjectManager(); - $searchBuilder = $objectManager->create('Magento\Framework\Api\SearchCriteriaBuilder'); + $searchBuilder = $objectManager->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); // Filter for 'firstname' like 'First' - $filterBuilder = $objectManager->create('Magento\Framework\Api\FilterBuilder'); + $filterBuilder = $objectManager->create(\Magento\Framework\Api\FilterBuilder::class); $firstnameFilter = $filterBuilder->setField('firstname') ->setConditionType('like') ->setValue('First%') ->create(); $searchBuilder->addFilters([$firstnameFilter]); // Search ascending order - $sortOrderBuilder = $objectManager->create('Magento\Framework\Api\SortOrderBuilder'); + $sortOrderBuilder = $objectManager->create(\Magento\Framework\Api\SortOrderBuilder::class); $sortOrder = $sortOrderBuilder ->setField('lastname') ->setDirection(SortOrder::SORT_ASC) @@ -397,7 +409,7 @@ public function testDeleteById() $this->customerRepository->deleteById($fixtureCustomerId); /** Ensure that customer was deleted */ $this->setExpectedException( - 'Magento\Framework\Exception\NoSuchEntityException', + \Magento\Framework\Exception\NoSuchEntityException::class, 'No such entity with email = customer@example.com, websiteId = 1' ); $this->customerRepository->get($fixtureCustomerEmail); @@ -424,7 +436,7 @@ public function updateCustomerDataProvider() public function searchCustomersDataProvider() { - $builder = Bootstrap::getObjectManager()->create('\Magento\Framework\Api\FilterBuilder'); + $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); return [ 'Customer with specific email' => [ [$builder->setField('email')->setValue('customer@search.example.com')->create()], @@ -476,7 +488,7 @@ protected function expectedDefaultShippingsInCustomerModelAttributes( /** * @var \Magento\Customer\Model\Customer $customer */ - $customer = $this->objectManager->create('Magento\Customer\Model\Customer'); + $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class); /** @var \Magento\Customer\Model\Customer $customer */ $customer->load($customerId); $this->assertEquals( @@ -490,4 +502,42 @@ protected function expectedDefaultShippingsInCustomerModelAttributes( 'default_shipping customer attribute did not updated' ); } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDbIsolation enabled + */ + public function testUpdateDefaultShippingAndDefaultBillingTest() + { + $customerId = 1; + $customerData = [ + "id" => 1, + "website_id" => 1, + "email" => "roni_cost@example.com", + "firstname" => "1111", + "lastname" => "Boss", + "middlename" => null, + "gender" => 0 + ]; + + $customerEntity = $this->customerFactory->create(['data' => $customerData]); + + $customer = $this->customerRepository->getById($customerId); + $oldDefaultBilling = $customer->getDefaultBilling(); + $oldDefaultShipping = $customer->getDefaultShipping(); + + $savedCustomer = $this->customerRepository->save($customerEntity); + + $this->assertEquals( + $savedCustomer->getDefaultBilling(), + $oldDefaultBilling, + 'Default billing shoud not be overridden' + ); + + $this->assertEquals( + $savedCustomer->getDefaultShipping(), + $oldDefaultShipping, + 'Default shipping shoud not be overridden' + ); + } } From 1cf49a851810822d9c842ab25fda7c44055fbddc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 8 May 2017 11:50:35 +0300 Subject: [PATCH 051/363] MAGETWO-61797: [Backport] - [Magento Cloud] - Deleting image in the admin panel deletes it in the server and causes error for other products - for 2.1 --- .../Model/Product/Gallery/UpdateHandler.php | 5 +- .../Model/ResourceModel/Product/Gallery.php | 18 +++++++ .../ResourceModel/Product/GalleryTest.php | 52 +++++++++++++++---- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 8c2d4a53a7722..03c175e5ffc73 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -29,7 +29,10 @@ protected function processDeletedImages($product, array &$images) if (!empty($image['removed'])) { if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { $recordsToDelete[] = $image['value_id']; - $filesToDelete[] = ltrim($image['file'], '/'); + // only delete physical files if they are not used by any other products + if (!($this->resourceModel->countImageUses($image['file']) > 1)) { + $filesToDelete[] = ltrim($image['file'], '/'); + } } } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index ad733d510b315..156f5672351fc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -449,4 +449,22 @@ public function getProductImages($product, $storeIds) return $this->getConnection()->fetchAll($select); } + + /** + * Counts uses of this image. + * + * @param string $image + * @return int + */ + public function countImageUses($image) + { + $select = $this->getConnection()->select() + ->from( + [$this->getMainTableAlias() => $this->getMainTable()], + 'count(*)' + ) + ->where('value = ?', $image); + + return $this->getConnection()->fetchOne($select); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php index 0518cc9ac5066..ce0b2fe6b36c4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php @@ -53,7 +53,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->connection = $this->getMock( - 'Magento\Framework\DB\Adapter\Pdo\Mysql', + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, [], [], '', @@ -63,7 +63,7 @@ protected function setUp() ->method('setCacheAdapter'); $metadata = $this->getMock( - 'Magento\Framework\EntityManager\EntityMetadata', + \Magento\Framework\EntityManager\EntityMetadata::class, [], [], '', @@ -77,7 +77,7 @@ protected function setUp() ->willReturn($this->connection); $metadataPool = $this->getMock( - 'Magento\Framework\EntityManager\MetadataPool', + \Magento\Framework\EntityManager\MetadataPool::class, [], [], '', @@ -85,21 +85,27 @@ protected function setUp() ); $metadataPool->expects($this->once()) ->method('getMetadata') - ->with('Magento\Catalog\Api\Data\ProductInterface') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) ->willReturn($metadata); - $resource = $this->getMock('Magento\Framework\App\ResourceConnection', [], [], '', false); + $resource = $this->getMock(\Magento\Framework\App\ResourceConnection::class, [], [], '', false); $resource->expects($this->any())->method('getTableName')->willReturn('table'); $this->resource = $objectManager->getObject( - 'Magento\Catalog\Model\ResourceModel\Product\Gallery', + \Magento\Catalog\Model\ResourceModel\Product\Gallery::class, [ 'metadataPool' => $metadataPool, 'resource' => $resource ] ); - $this->product = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); - $this->select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); - $this->attribute = $this->getMock('Magento\Eav\Model\Entity\Attribute\AbstractAttribute', [], [], '', false); + $this->product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $this->select = $this->getMock(\Magento\Framework\DB\Select::class, [], [], '', false); + $this->attribute = $this->getMock( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + [], + '', + false + ); } public function testLoadDataFromTableByValueId() @@ -437,4 +443,32 @@ public function testDeleteGalleryValueInStore() $this->resource->deleteGalleryValueInStore($valueId, $entityId, $storeId); } + + public function testCountImageUses() + { + $results = [ + [ + 'value_id' => '1', + 'attribute_id' => 90, + 'value' => '/d/o/download_7.jpg', + 'media_type' => 'image', + 'disabled' => '0', + ], + ]; + + $this->connection->expects($this->once())->method('select')->will($this->returnValue($this->select)); + $this->select->expects($this->at(0)) + ->method('from') + ->with(['main' => 'table'], 'count(*)') + ->willReturnSelf(); + $this->select->expects($this->at(1)) + ->method('where') + ->with('value = ?', 1) + ->willReturnSelf(); + $this->connection->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn(count($results)); + $this->assertEquals($this->resource->countImageUses(1), count($results)); + } } From 048d2e6991d964f8ee770a3377cc5fde62802f63 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 8 May 2017 13:54:24 +0300 Subject: [PATCH 052/363] MAGETWO-58876: [BP][Cloud] Mass actions are slow and consume excessive memory when merchandizing for 2.1.x --- .../Ui/Component/MassAction/Filter.php | 91 +++++- .../Unit/Component/MassAction/FilterTest.php | 304 ++++++++++++++++++ 2 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php diff --git a/app/code/Magento/Ui/Component/MassAction/Filter.php b/app/code/Magento/Ui/Component/MassAction/Filter.php index c598caa93927f..166b694a94f93 100644 --- a/app/code/Magento/Ui/Component/MassAction/Filter.php +++ b/app/code/Magento/Ui/Component/MassAction/Filter.php @@ -5,12 +5,14 @@ */ namespace Magento\Ui\Component\MassAction; +use Magento\Framework\Data\Collection; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\App\RequestInterface; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; /** * Class Filter @@ -43,6 +45,11 @@ class Filter */ protected $filterBuilder; + /** + * @var DataProviderInterface + */ + private $dataProvider; + /** * @param UiComponentFactory $factory * @param RequestInterface $request @@ -70,27 +77,39 @@ public function getComponent() if (!isset($this->components[$namespace])) { $this->components[$namespace] = $this->factory->create($namespace); } + return $this->components[$namespace]; } /** + * Adds filters to collection using DataProvider filter results + * * @param AbstractDb $collection * @return AbstractDb * @throws LocalizedException */ public function getCollection(AbstractDb $collection) { - $component = $this->getComponent(); - $this->prepareComponent($component); - $dataProvider = $component->getContext()->getDataProvider(); - $dataProvider->setLimit(0, false); - $ids = []; - foreach ($dataProvider->getSearchResult()->getItems() as $document) { - $ids[] = $document->getId(); + $selected = $this->request->getParam(static::SELECTED_PARAM); + $excluded = $this->request->getParam(static::EXCLUDED_PARAM); + + $isExcludedIdsValid = (is_array($excluded) && !empty($excluded)); + $isSelectedIdsValid = (is_array($selected) && !empty($selected)); + + if ('false' !== $excluded) { + if (!$isExcludedIdsValid && !$isSelectedIdsValid) { + throw new LocalizedException(__('Please select item(s).')); + } + } + $idsArray = $this->getFilterIds(); + if (!empty($idsArray)) { + $collection->addFieldToFilter( + $collection->getIdFieldName(), + ['in' => $idsArray] + ); } - $collection->addFieldToFilter($collection->getIdFieldName(), ['in' => $ids]); - return $this->applySelection($collection); + return $collection; } /** @@ -103,12 +122,12 @@ public function applySelectionOnTargetProvider() { $selected = $this->request->getParam(static::SELECTED_PARAM); $excluded = $this->request->getParam(static::EXCLUDED_PARAM); + if ('false' === $excluded) { return; } - $component = $this->getComponent(); - $this->prepareComponent($component); - $dataProvider = $component->getContext()->getDataProvider(); + + $dataProvider = $this->getDataProvider(); try { if (is_array($excluded) && !empty($excluded)) { $this->filterBuilder->setConditionType('nin') @@ -127,6 +146,8 @@ public function applySelectionOnTargetProvider() } /** + * Applies selection to collection from POST parameters + * * @param AbstractDb $collection * @return AbstractDb * @throws LocalizedException @@ -151,6 +172,7 @@ protected function applySelection(AbstractDb $collection) } catch (\Exception $e) { throw new LocalizedException(__($e->getMessage())); } + return $collection; } @@ -176,6 +198,51 @@ public function prepareComponent(UiComponentInterface $component) public function getComponentRefererUrl() { $data = $this->getComponent()->getContext()->getDataProvider()->getConfigData(); + return (isset($data['referer_url'])) ? $data['referer_url'] : null; } + + /** + * Get data provider + * + * @return DataProviderInterface + */ + private function getDataProvider() + { + if (!$this->dataProvider) { + $component = $this->getComponent(); + $this->prepareComponent($component); + $this->dataProvider = $component->getContext()->getDataProvider(); + $this->dataProvider->setLimit(0, false); + } + + return $this->dataProvider; + } + + /** + * Get filter ids as array + * + * @return int[] + */ + private function getFilterIds() + { + $ids = []; + $this->applySelectionOnTargetProvider(); + + /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ + $searchResult = $this->getDataProvider()->getSearchResult(); + + if ($searchResult) { + if ($searchResult instanceof Collection + || method_exists($searchResult, 'getAllIds')) { + $ids = $searchResult->getAllIds(); + } else { + foreach ($searchResult->getItems() as $document) { + $ids[] = $document->getId(); + } + } + } + + return $ids; + } } diff --git a/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php b/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php new file mode 100644 index 0000000000000..3e419a1bc7157 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php @@ -0,0 +1,304 @@ +objectManager = new ObjectManager($this); + $this->uiComponentFactoryMock = $this->getMock(UiComponentFactory::class, [], [], '', false); + $this->filterBuilderMock = $this->getMock(FilterBuilder::class, [], [], '', false); + $this->requestMock = $this->getMock(RequestInterface::class); + $this->dataProviderMock = $this->getMock(DataProviderInterface::class); + $this->uiComponentMock = $this->getMock(UiComponentInterface::class); + $this->abstractDbMock = $this->getMock(AbstractDb::class, [], [], '', false); + $contextMock = $this->getMock(ContextInterface::class); + $uiComponentMockTwo = $this->getMock(UiComponentInterface::class); + $this->filter = $this->objectManager->getObject( + Filter::class, + [ + 'factory' => $this->uiComponentFactoryMock, + 'request' => $this->requestMock, + 'filterBuilder' => $this->filterBuilderMock + ] + ); + $this->uiComponentFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->uiComponentMock); + $this->uiComponentMock->expects($this->any()) + ->method('getChildComponents') + ->willReturn([$uiComponentMockTwo]); + $uiComponentMockTwo->expects($this->any()) + ->method('getChildComponents') + ->willReturn([]); + $this->uiComponentMock->expects($this->any()) + ->method('getContext') + ->willReturn($contextMock); + $contextMock->expects($this->any()) + ->method('getDataProvider') + ->willReturn($this->dataProviderMock); + $this->dataProviderMock->expects($this->any()) + ->method('setLimit'); + } + + /** + * This tests the method getComponent() + */ + public function testGetComponent() + { + $this->requestMock->expects($this->at(0)) + ->method('getParam') + ->with('namespace') + ->willReturn(''); + $this->assertEquals($this->uiComponentMock, $this->filter->getComponent()); + } + + /** + * Run test for getCollection method + * + * @param int[]|bool $selectedIds + * @param int[]|bool $excludedIds + * @param int $filterExpected + * @param string $conditionExpected + * @param string $collectionClass + * @dataProvider applySelectionOnTargetProviderDataProvider + */ + public function testGetCollection($selectedIds, $excludedIds, $filterExpected, $conditionExpected, $collectionClass) + { + $this->setUpApplySelection($selectedIds, $excludedIds, $filterExpected, $conditionExpected); + $this->requestMock->expects($this->at(4)) + ->method('getParam') + ->with('namespace') + ->willReturn(''); + $this->requestMock->expects($this->at(2)) + ->method('getParam') + ->with(Filter::SELECTED_PARAM) + ->willReturn($selectedIds); + $this->requestMock->expects($this->at(3)) + ->method('getParam') + ->with(Filter::EXCLUDED_PARAM) + ->willReturn($excludedIds); + $this->searchResultMock = $this->getMockBuilder($collectionClass) + ->disableOriginalConstructor() + ->setMethods(['getItems', 'getAllIds']) + ->getMockForAbstractClass(); + $this->searchResultMock->expects($this->any()) + ->method('getItems') + ->willReturn([]); + $this->dataProviderMock->expects($this->any()) + ->method('getSearchResult') + ->willReturn($this->searchResultMock); + $this->searchResultMock->expects($this->any()) + ->method('getAllIds') + ->willReturn([]); + $this->assertEquals($this->abstractDbMock, $this->filter->getCollection($this->abstractDbMock)); + } + + /** + * Run test for applySelectionOnTargetProvider method + * + * @param int[]|bool $selectedIds + * @param int[]|bool $excludedIds + * @param int $filterExpected + * @param string $conditionExpected + * @dataProvider applySelectionOnTargetProviderDataProvider + */ + public function testApplySelectionOnTargetProvider($selectedIds, $excludedIds, $filterExpected, $conditionExpected) + { + $this->setUpApplySelection($selectedIds, $excludedIds, $filterExpected, $conditionExpected); + $this->filter->applySelectionOnTargetProvider(); + } + + /** + * Data provider for testApplySelectionOnTargetProvider + */ + public function applySelectionOnTargetProviderDataProvider() + { + return [ + [[1, 2, 3], 'false' , 0, 'in', SearchResultInterface::class], + [[1, 2, 3], [1, 2, 3] , 1, 'nin', SearchResultInterface::class], + ['false', [1, 2, 3] , 1, 'nin', SearchResultInterface::class], + ['false', 'false' , 0, '', SearchResultInterface::class], + [[1, 2, 3], 'false' , 0, 'in', Collection::class], + [[1, 2, 3], [1, 2, 3] , 1, 'nin', Collection::class], + ['false', [1, 2, 3] , 1, 'nin', Collection::class], + ['false', 'false' , 0, '', Collection::class] + ]; + } + + /** + * @throws \Exception + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testApplySelectionOnTargetProviderException() + { + $filterMock = $this->getMock(ApiFilter::class, [], [], '', false); + $this->filterBuilderMock->expects($this->any()) + ->method('setConditionType') + ->willReturn($this->filterBuilderMock); + $this->filterBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($filterMock); + $this->filterBuilderMock->expects($this->any()) + ->method('setField') + ->willReturn($this->filterBuilderMock); + $this->requestMock->expects($this->at(0)) + ->method('getParam') + ->with(Filter::SELECTED_PARAM) + ->willReturn([1]); + $this->requestMock->expects($this->at(1)) + ->method('getParam') + ->with(Filter::EXCLUDED_PARAM) + ->willReturn([]); + $this->dataProviderMock->expects($this->any()) + ->method('addFilter') + ->with($filterMock) + ->willThrowException(new \Exception('exception')); + $this->filter->applySelectionOnTargetProvider(); + } + + /** + * This tests the method prepareComponent() + */ + public function testPrepareComponent() + { + $this->filter->prepareComponent($this->uiComponentMock); + } + + /** + * This tests the method getComponentRefererUrl() + */ + public function testGetComponentRefererUrlIsNotNull() + { + $returnArray = [ + 'referer_url' => 'referer_url' + ]; + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn($returnArray); + $this->assertEquals('referer_url', $this->filter->getComponentRefererUrl()); + } + + /** + * This tests the method getComponentRefererUrl() + */ + public function testGetComponentRefererUrlIsNull() + { + $this->assertNull($this->filter->getComponentRefererUrl()); + } + + /** + * Apply mocks for current parameters from datasource + * + * @param int[]|bool $selectedIds + * @param int[]|bool $excludedIds + * @param int $filterExpected + * @param string $conditionExpected + */ + private function setUpApplySelection($selectedIds, $excludedIds, $filterExpected, $conditionExpected) + { + $filterMock = $this->getMock(ApiFilter::class, [], [], '', false); + $this->requestMock->expects($this->at(0)) + ->method('getParam') + ->with(Filter::SELECTED_PARAM) + ->willReturn($selectedIds); + $this->requestMock->expects($this->at(1)) + ->method('getParam') + ->with(Filter::EXCLUDED_PARAM) + ->willReturn($excludedIds); + $this->dataProviderMock->expects($this->exactly($filterExpected)) + ->method('addFilter') + ->with($filterMock); + $this->filterBuilderMock->expects($this->exactly($filterExpected)) + ->method('setConditionType') + ->with($conditionExpected) + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('value') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($filterMock); + $this->filterBuilderMock->expects($this->any()) + ->method('setConditionType') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('value') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($filterMock); + } +} From 0ca5a204da6ae66936e39db859846c06f60c24b5 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 8 May 2017 14:09:25 +0300 Subject: [PATCH 053/363] MAGETWO-58876: [BP][Cloud] Mass actions are slow and consume excessive memory when merchandizing for 2.1.x --- .../Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php b/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php index 3e419a1bc7157..23ce14a9d8491 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/MassAction/FilterTest.php @@ -1,6 +1,6 @@ Date: Mon, 8 May 2017 15:21:48 +0300 Subject: [PATCH 054/363] MAGETWO-63020: [2.1 backport] SCD does not work when multiple languages are specified. --- .../Theme/Model/Theme/Domain/VirtualTest.php | 14 ++++++++++---- .../View/Design/Theme/FlyweightFactory.php | 16 ++++++++++------ .../Unit/Design/Theme/FlyweightFactoryTest.php | 18 +++++++++++------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/Domain/VirtualTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/Domain/VirtualTest.php index ab02007ead964..0d8e4802daca4 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/Domain/VirtualTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/Domain/VirtualTest.php @@ -7,6 +7,9 @@ use Magento\Framework\View\Design\ThemeInterface; +/** + * Virtual theme test + */ class VirtualTest extends \PHPUnit_Framework_TestCase { /** @@ -19,6 +22,7 @@ class VirtualTest extends \PHPUnit_Framework_TestCase 'theme_title' => 'Test physical theme', 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'type' => ThemeInterface::TYPE_PHYSICAL, + 'code' => 'physical', ], 'virtual' => [ 'parent_id' => null, @@ -26,6 +30,7 @@ class VirtualTest extends \PHPUnit_Framework_TestCase 'theme_title' => 'Test virtual theme', 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'type' => ThemeInterface::TYPE_VIRTUAL, + 'code' => 'virtual', ], 'staging' => [ 'parent_id' => null, @@ -33,6 +38,7 @@ class VirtualTest extends \PHPUnit_Framework_TestCase 'theme_title' => 'Test staging theme', 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'type' => ThemeInterface::TYPE_STAGING, + 'code' => 'staging', ], ]; @@ -54,21 +60,21 @@ public function testGetPhysicalTheme() $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); //1. set up fixture /** @var $physicalTheme \Magento\Framework\View\Design\ThemeInterface */ - $physicalTheme = $objectManager->create('Magento\Framework\View\Design\ThemeInterface'); + $physicalTheme = $objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class); $physicalTheme->setData($this->_themes['physical']); $physicalTheme->save(); $this->_themes['virtual']['parent_id'] = $physicalTheme->getId(); /** @var $virtualTheme \Magento\Framework\View\Design\ThemeInterface */ - $virtualTheme = $objectManager->create('Magento\Framework\View\Design\ThemeInterface'); + $virtualTheme = $objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class); $virtualTheme->setData($this->_themes['virtual']); $virtualTheme->save(); $this->_themes['staging']['parent_id'] = $virtualTheme->getId(); /** @var $stagingTheme \Magento\Framework\View\Design\ThemeInterface */ - $stagingTheme = $objectManager->create('Magento\Framework\View\Design\ThemeInterface'); + $stagingTheme = $objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class); $stagingTheme->setData($this->_themes['staging']); $stagingTheme->save(); @@ -77,7 +83,7 @@ public function testGetPhysicalTheme() //2. run test /** @var $virtualTheme \Magento\Framework\View\Design\ThemeInterface */ - $virtualTheme = $objectManager->create('Magento\Framework\View\Design\ThemeInterface'); + $virtualTheme = $objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class); $virtualTheme->load($this->_virtualThemeId); $this->assertEquals( diff --git a/lib/internal/Magento/Framework/View/Design/Theme/FlyweightFactory.php b/lib/internal/Magento/Framework/View/Design/Theme/FlyweightFactory.php index 30ae8581197e6..4aad99564075a 100644 --- a/lib/internal/Magento/Framework/View/Design/Theme/FlyweightFactory.php +++ b/lib/internal/Magento/Framework/View/Design/Theme/FlyweightFactory.php @@ -44,11 +44,15 @@ public function __construct(ThemeProviderInterface $themeProvider) /** * Creates or returns a shared model of theme * - * @param string $themeKey - * @param string $area - * @return \Magento\Framework\View\Design\ThemeInterface|null - * @throws \InvalidArgumentException - * @throws \LogicException + * Search for theme in File System by specific path or load theme from DB + * by specific path (e.g. adminhtml/Magento/backend) or by identifier (theme primary key) and return it + * Can be used to deploy static or in other setup commands, even if Magento is not installed yet. + * + * @param string $themeKey Should looks like Magento/backend or should be theme primary key + * @param string $area Can be adminhtml, frontend, etc... + * @return \Magento\Framework\View\Design\ThemeInterface + * @throws \InvalidArgumentException when incorrect themeKey was specified + * @throws \LogicException when theme with appropriate themeKey was not found */ public function create($themeKey, $area = \Magento\Framework\View\DesignInterface::DEFAULT_AREA) { @@ -61,7 +65,7 @@ public function create($themeKey, $area = \Magento\Framework\View\DesignInterfac } else { $themeModel = $this->_loadByPath($themeKey, $area); } - if (!$themeModel->getId()) { + if (!$themeModel->getCode()) { throw new \LogicException("Unable to load theme by specified key: '{$themeKey}'"); } $this->_addTheme($themeModel); diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/FlyweightFactoryTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/FlyweightFactoryTest.php index 9e4f1dfe16560..a8f5a3deaa0f9 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/FlyweightFactoryTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/FlyweightFactoryTest.php @@ -7,6 +7,9 @@ use \Magento\Framework\View\Design\Theme\FlyweightFactory; +/** + * FlyweightFactory test class + */ class FlyweightFactoryTest extends \PHPUnit_Framework_TestCase { /** @@ -21,7 +24,7 @@ class FlyweightFactoryTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->themeProviderMock = $this->getMock('Magento\Framework\View\Design\Theme\ThemeProviderInterface'); + $this->themeProviderMock = $this->getMock(\Magento\Framework\View\Design\Theme\ThemeProviderInterface::class); $this->factory = new FlyweightFactory($this->themeProviderMock); } @@ -33,10 +36,11 @@ protected function setUp() */ public function testCreateById($path, $expectedId) { - $theme = $this->getMock('Magento\Theme\Model\Theme', [], [], '', false); - $theme->expects($this->exactly(3))->method('getId')->will($this->returnValue($expectedId)); + $theme = $this->getMock(\Magento\Theme\Model\Theme::class, [], [], '', false); + $theme->expects($this->exactly(2))->method('getId')->will($this->returnValue($expectedId)); $theme->expects($this->once())->method('getFullPath')->will($this->returnValue(null)); + $theme->expects($this->once())->method('getCode')->willReturn($expectedId); $this->themeProviderMock->expects( $this->once() @@ -69,10 +73,11 @@ public function testCreateByPath() { $path = 'frontend/Magento/luma'; $themeId = 7; - $theme = $this->getMock('Magento\Theme\Model\Theme', [], [], '', false); - $theme->expects($this->exactly(3))->method('getId')->will($this->returnValue($themeId)); + $theme = $this->getMock(\Magento\Theme\Model\Theme::class, [], [], '', false); + $theme->expects($this->exactly(2))->method('getId')->will($this->returnValue($themeId)); $theme->expects($this->once())->method('getFullPath')->will($this->returnValue($path)); + $theme->expects($this->once())->method('getCode')->willReturn('Magento/luma'); $this->themeProviderMock->expects( $this->once() @@ -94,8 +99,7 @@ public function testCreateByPath() public function testCreateDummy() { $themeId = 0; - $theme = $this->getMock('Magento\Theme\Model\Theme', [], [], '', false); - $theme->expects($this->once())->method('getId')->will($this->returnValue($themeId)); + $theme = $this->getMock(\Magento\Theme\Model\Theme::class, [], [], '', false); $this->themeProviderMock->expects( $this->once() From d0ed6ed907e1309cc1c1a1d81e25766de09e35e2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 8 May 2017 16:32:43 +0300 Subject: [PATCH 055/363] MAGETWO-61027: [Backport] - "Product export duplicate rows for product with html special chars in data" for 2.1.x --- .../Model/Export/Product.php | 4 +- .../products_with_multiselect_attribute.php | 94 +++++++++---------- .../AbstractProductExportImportTestCase.php | 1 + .../Model/Export/ProductTest.php | 17 ++++ .../product_export_data_special_chars.php | 38 ++++++++ ...uct_export_data_special_chars_rollback.php | 10 ++ 6 files changed, 112 insertions(+), 52 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 57a3e56153d26..b3f266728e952 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -944,7 +944,7 @@ protected function collectRawData() if ($storeId != Store::DEFAULT_STORE_ID && isset($data[$itemId][Store::DEFAULT_STORE_ID][$fieldName]) - && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == $attrValue + && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == htmlspecialchars_decode($attrValue) ) { continue; } @@ -983,7 +983,7 @@ protected function collectRawData() $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); } - $data[$itemId][$storeId][self::COL_SKU] = $item->getSku(); + $data[$itemId][$storeId][self::COL_SKU] = htmlspecialchars_decode($item->getSku()); $data[$itemId][$storeId]['store_id'] = $storeId; $data[$itemId][$storeId]['product_id'] = $itemId; $data[$itemId][$storeId]['product_link_id'] = $productLinkId; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php index cadeadf255276..550a432f11f8f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php @@ -12,62 +12,56 @@ /** Create product with options and multiselect attribute */ /** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Setup\CategorySetup'); +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Setup\CategorySetup::class); /** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ $options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection' + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class ); $options->setAttributeFilter($attribute->getId()); $optionIds = $options->getAllIds(); /** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); -$product->setTypeId( - \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE -)->setId( - $optionIds[0] * 10 -)->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') -)->setWebsiteIds( - [1] -)->setName( - 'With Multiselect 1' -)->setSku( - 'simple_ms_1' -)->setPrice( - 10 -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setMultiselectAttribute( - [$optionIds[0]] -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setStockData( - ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] -)->save(); +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1') + ->setSku('simple_ms_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[0]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1 + ] + ) + ->save(); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); -$product->setTypeId( - \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE -)->setId( - $optionIds[1] * 10 -)->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') -)->setWebsiteIds( - [1] -)->setName( - 'With Multiselect 2' -)->setSku( - 'simple_ms_2' -)->setPrice( - 10 -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setMultiselectAttribute( - [$optionIds[1], $optionIds[2], $optionIds[3]] -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setStockData( - ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] -)->save(); +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 2') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[1], $optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1 + ] + ) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index d2cceb1087471..f9b15df553e4f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -56,6 +56,7 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit_Framework_Te 'custom_design_from', 'updated_in', 'tax_class_id', + 'description' ]; protected function setUp() diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 76381c7cdc401..1471d7b8ad75e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -87,6 +87,23 @@ public function testExport() $this->assertContains('test_option_code_2', $exportData); $this->assertContains('max_characters=10', $exportData); $this->assertContains('text_attribute=!@#$%^&*()_+1234567890-=|\\:;""\'<,>.?/', $exportData); + $occurrencesCount = substr_count($exportData, 'Hello "" &"" Bring the water bottle when you can!'); + $this->assertEquals(1, $occurrencesCount); + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data_special_chars.php + * @magentoDbIsolationEnabled + */ + public function testExportSpecialChars() + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + $this->assertContains('simple ""1""', $exportData); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php new file mode 100644 index 0000000000000..a7c7aff597c2b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php @@ -0,0 +1,38 @@ +create(\Magento\Catalog\Model\Product::class); + +$productModel->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setName('New Product') + ->setSku('simple "1"') + ->setPrice(10) + ->addData(['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/']) + ->setTierPrice([0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCateroryIds([]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1]) + ->setCanSaveCustomOptions(true) + ->setCategoryIds([333]) + ->setUpSellLinkData([$product->getId() => ['position' => 1]]); + +$productModel->setOptions([])->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php new file mode 100644 index 0000000000000..a4769078a7713 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -0,0 +1,10 @@ + Date: Mon, 8 May 2017 17:18:24 +0300 Subject: [PATCH 056/363] MAGETWO-64241: Magento\ProductVideo\Test\TestCase\UpdateProductVideoTest.test with data set "GetVideoInfoTestVariation2" --- .../Test/TestCase/UpdateProductVideoTest.xml | 4 ++-- .../TestSuite/InjectableTests/MAGETWO-64241.xml | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml diff --git a/dev/tests/functional/tests/app/Magento/ProductVideo/Test/TestCase/UpdateProductVideoTest.xml b/dev/tests/functional/tests/app/Magento/ProductVideo/Test/TestCase/UpdateProductVideoTest.xml index e3922e303d571..a21202d4c5738 100644 --- a/dev/tests/functional/tests/app/Magento/ProductVideo/Test/TestCase/UpdateProductVideoTest.xml +++ b/dev/tests/functional/tests/app/Magento/ProductVideo/Test/TestCase/UpdateProductVideoTest.xml @@ -52,8 +52,8 @@ test_type:extended_acceptance_test product_with_video_vimeo simple_product_with_category_%isolation% - https://vimeo.com/21776334 - Foo Fighters - "Walk" - Official Music Video (HD) + https://vimeo.com/103756396 + XO Stereo - Show And Tell OFFICIAL MUSIC VIDEO play_if_base diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml new file mode 100644 index 0000000000000..94c1eb696face --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file From 6547fabfd9bbbdc355a79de0ead0f44413fe0ef4 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Mon, 8 May 2017 18:34:54 +0300 Subject: [PATCH 057/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../ResourceModel/CustomerRepository.php | 68 +++++++++++-------- .../ResourceModel/CustomerRepositoryTest.php | 35 ++++++++-- 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 7a1c6c0bbd46b..0a1b03a973a33 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -15,7 +15,6 @@ /** * Customer repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface { @@ -132,8 +131,6 @@ public function __construct( /** * {@inheritdoc} - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) { @@ -218,32 +215,7 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); - if ($customer->getAddresses() !== null) { - if ($customer->getId()) { - $existingAddresses = $this->getById($customer->getId())->getAddresses(); - $getIdFunc = function ($address) { - return $address->getId(); - }; - $existingAddressIds = array_map($getIdFunc, $existingAddresses); - } else { - $existingAddressIds = []; - } - - $savedAddressIds = []; - foreach ($customer->getAddresses() as $address) { - $address->setCustomerId($customerId) - ->setRegion($address->getRegion()); - $this->addressRepository->save($address); - if ($address->getId()) { - $savedAddressIds[] = $address->getId(); - } - } - - $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); - foreach ($addressIdsToDelete as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } + $this->updateAddresses($customer, $customerId); $savedCustomer = $this->get($customer->getEmail(), $customer->getWebsiteId()); $this->eventManager->dispatch( @@ -363,4 +335,42 @@ protected function addFilterGroupToCollection( $collection->addFieldToFilter($fields); } } + + /** + * Update customer addresses. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param $customerId + * @return void + * @throws \Magento\Framework\Exception\InputException + */ + private function updateAddresses(\Magento\Customer\Api\Data\CustomerInterface $customer, $customerId) + { + if ($customer->getAddresses() !== null) { + if ($customer->getId()) { + $existingAddresses = $this->getById($customer->getId())->getAddresses(); + $getIdFunc = function ($address) { + return $address->getId(); + }; + $existingAddressIds = array_map($getIdFunc, $existingAddresses); + } else { + $existingAddressIds = []; + } + + $savedAddressIds = []; + foreach ($customer->getAddresses() as $address) { + $address->setCustomerId($customerId) + ->setRegion($address->getRegion()); + $this->addressRepository->save($address); + if ($address->getId()) { + $savedAddressIds[] = $address->getId(); + } + } + + $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); + foreach ($addressIdsToDelete as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 3406de3d47eaf..fb7f2720299be 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -76,6 +76,8 @@ protected function tearDown() } /** + * Test create new customer new then update first name. + * * @magentoDbIsolation enabled */ public function testCreateCustomerNewThenUpdateFirstName() @@ -110,6 +112,8 @@ public function testCreateCustomerNewThenUpdateFirstName() } /** + * Test create new customer. + * * @magentoDbIsolation enabled */ public function testCreateNewCustomer() @@ -137,6 +141,8 @@ public function testCreateNewCustomer() } /** + * Test update customer. + * * @dataProvider updateCustomerDataProvider * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php @@ -212,6 +218,8 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) } /** + * Test update customer address. + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -253,6 +261,8 @@ public function testUpdateCustomerAddress() } /** + * Test update customer preserve all addresses. + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -278,6 +288,8 @@ public function testUpdateCustomerPreserveAllAddresses() } /** + * Test update customer delete all addresses with empty array. + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -303,6 +315,8 @@ public function testUpdateCustomerDeleteAllAddressesWithEmptyArray() } /** + * Test search customers. + * * @param \Magento\Framework\Api\Filter[] $filters * @param \Magento\Framework\Api\Filter[] $filterGroup * @param array $expectedResult array of expected results indexed by ID @@ -336,7 +350,7 @@ public function testSearchCustomers($filters, $filterGroup, $expectedResult) } /** - * Test ordering + * Test search customers order. * * @magentoDataFixture Magento/Customer/_files/three_customers.php * @magentoDbIsolation enabled @@ -380,6 +394,8 @@ public function testSearchCustomersOrder() } /** + * Test delete. + * * @magentoAppArea adminhtml * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoAppIsolation enabled @@ -398,6 +414,8 @@ public function testDelete() } /** + * Test delete by id. + * * @magentoAppArea adminhtml * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoAppIsolation enabled @@ -416,7 +434,7 @@ public function testDeleteById() } /** - * DataProvider update customer + * DataProvider update customer. * * @return array */ @@ -434,6 +452,11 @@ public function updateCustomerDataProvider() ]; } + /** + * DataProvider search customers. + * + * @return array + */ public function searchCustomersDataProvider() { $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); @@ -474,7 +497,7 @@ public function searchCustomersDataProvider() } /** - * Check defaults billing and shipping in customer model + * Check default shippings in customer model. * * @param $customerId * @param $defaultBilling @@ -504,6 +527,8 @@ protected function expectedDefaultShippingsInCustomerModelAttributes( } /** + * Test update default shipping and default billing address. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDbIsolation enabled */ @@ -531,13 +556,13 @@ public function testUpdateDefaultShippingAndDefaultBillingTest() $this->assertEquals( $savedCustomer->getDefaultBilling(), $oldDefaultBilling, - 'Default billing shoud not be overridden' + 'Default billing should not be overridden' ); $this->assertEquals( $savedCustomer->getDefaultShipping(), $oldDefaultShipping, - 'Default shipping shoud not be overridden' + 'Default shipping should not be overridden' ); } } From e076de2264c49b7bf6c1dbd19517ebf413b080d0 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 09:05:00 +0300 Subject: [PATCH 058/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../ResourceModel/CustomerRepository.php | 101 +++++++++++++----- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 0a1b03a973a33..d49eb1fe4a2b2 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -130,6 +130,8 @@ public function __construct( } /** + * Save. + * * {@inheritdoc} */ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) @@ -175,19 +177,7 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa ); } // Populate model with secure data - if ($customer->getId()) { - $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); - $customerModel->setRpToken($customerSecure->getRpToken()); - $customerModel->setRpTokenCreatedAt($customerSecure->getRpTokenCreatedAt()); - $customerModel->setPasswordHash($customerSecure->getPasswordHash()); - $customerModel->setFailuresNum($customerSecure->getFailuresNum()); - $customerModel->setFirstFailure($customerSecure->getFirstFailure()); - $customerModel->setLockExpires($customerSecure->getLockExpires()); - } else { - if ($passwordHash) { - $customerModel->setPasswordHash($passwordHash); - } - } + $this->populateCustomerModelWithSecureData($customer, $passwordHash, $customerModel); // If customer email was changed, reset RpToken info if ($prevCustomerData @@ -197,19 +187,9 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $customerModel->setRpTokenCreatedAt(null); } - if (!array_key_exists('default_billing', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_billing', $prevCustomerDataArr) - ) { - $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); - } + $this->setDefaultBilling($customerArr, $prevCustomerDataArr, $customerModel); - if (!array_key_exists('default_shipping', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_shipping', $prevCustomerDataArr) - ) { - $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); - } + $this->setDefaultShipping($customerArr, $prevCustomerDataArr, $customerModel); $customerModel->save(); $this->customerRegistry->push($customerModel); @@ -226,6 +206,8 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa } /** + * Get. + * * {@inheritdoc} */ public function get($email, $websiteId = null) @@ -235,6 +217,8 @@ public function get($email, $websiteId = null) } /** + * Get by Id. + * * {@inheritdoc} */ public function getById($customerId) @@ -244,6 +228,8 @@ public function getById($customerId) } /** + * Get list. + * * {@inheritdoc} */ public function getList(SearchCriteriaInterface $searchCriteria) @@ -296,6 +282,8 @@ public function getList(SearchCriteriaInterface $searchCriteria) } /** + * Delete. + * * {@inheritdoc} */ public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer) @@ -304,6 +292,8 @@ public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer) } /** + * Delete by Id. + * * {@inheritdoc} */ public function deleteById($customerId) @@ -373,4 +363,65 @@ private function updateAddresses(\Magento\Customer\Api\Data\CustomerInterface $c } } } + + /** + * Populate customer model with secure data. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param $passwordHash + * @param $customerModel + */ + private function populateCustomerModelWithSecureData( + \Magento\Customer\Api\Data\CustomerInterface $customer, + $passwordHash, + $customerModel + ) { + if ($customer->getId()) { + $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); + $customerModel->setRpToken($customerSecure->getRpToken()); + $customerModel->setRpTokenCreatedAt($customerSecure->getRpTokenCreatedAt()); + $customerModel->setPasswordHash($customerSecure->getPasswordHash()); + $customerModel->setFailuresNum($customerSecure->getFailuresNum()); + $customerModel->setFirstFailure($customerSecure->getFirstFailure()); + $customerModel->setLockExpires($customerSecure->getLockExpires()); + } else { + if ($passwordHash) { + $customerModel->setPasswordHash($passwordHash); + } + } + } + + /** + * Set default billing. + * + * @param $customerArr + * @param $prevCustomerDataArr + * @param $customerModel + */ + private function setDefaultBilling($customerArr, $prevCustomerDataArr, $customerModel) + { + if (!array_key_exists('default_billing', $customerArr) && + null !== $prevCustomerDataArr && + array_key_exists('default_billing', $prevCustomerDataArr) + ) { + $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); + } + } + + /** + * Set default shipping. + * + * @param $customerArr + * @param $prevCustomerDataArr + * @param $customerModel + */ + private function setDefaultShipping($customerArr, $prevCustomerDataArr, $customerModel) + { + if (!array_key_exists('default_shipping', $customerArr) && + null !== $prevCustomerDataArr && + array_key_exists('default_shipping', $prevCustomerDataArr) + ) { + $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); + } + } } From 24bef5b1d2d9f96d98de5c7d0f844a2d5ebe2ae0 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 10 May 2017 09:13:16 +0300 Subject: [PATCH 059/363] MAGETWO-64241: Magento\ProductVideo\Test\TestCase\UpdateProductVideoTest.test with data set "GetVideoInfoTestVariation2" --- .../TestSuite/InjectableTests/MAGETWO-64241.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml deleted file mode 100644 index 94c1eb696face..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64241.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file From 5db3632949cff0f9a729d6357f684f95d3372db6 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 10:28:11 +0300 Subject: [PATCH 060/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../Customer/Model/ResourceModel/CustomerRepository.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index d49eb1fe4a2b2..166aeed7ae651 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -329,12 +329,12 @@ protected function addFilterGroupToCollection( /** * Update customer addresses. * - * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param \Magento\Framework\Api\CustomAttributesDataInterface $customer * @param $customerId * @return void * @throws \Magento\Framework\Exception\InputException */ - private function updateAddresses(\Magento\Customer\Api\Data\CustomerInterface $customer, $customerId) + private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInterface $customer, $customerId) { if ($customer->getAddresses() !== null) { if ($customer->getId()) { @@ -367,12 +367,12 @@ private function updateAddresses(\Magento\Customer\Api\Data\CustomerInterface $c /** * Populate customer model with secure data. * - * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param \Magento\Framework\Api\CustomAttributesDataInterface $customer * @param $passwordHash * @param $customerModel */ private function populateCustomerModelWithSecureData( - \Magento\Customer\Api\Data\CustomerInterface $customer, + \Magento\Framework\Api\CustomAttributesDataInterface $customer, $passwordHash, $customerModel ) { From 8f9ab1179685efa747feac3e42c7ffd00b60ee9a Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 11:31:39 +0300 Subject: [PATCH 061/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../ResourceModel/CustomerRepository.php | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 166aeed7ae651..81e2cdc2544f4 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -14,6 +14,7 @@ /** * Customer repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface @@ -330,11 +331,11 @@ protected function addFilterGroupToCollection( * Update customer addresses. * * @param \Magento\Framework\Api\CustomAttributesDataInterface $customer - * @param $customerId + * @param int $customerId * @return void * @throws \Magento\Framework\Exception\InputException */ - private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInterface $customer, $customerId) + private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInterface $customer, int $customerId) { if ($customer->getAddresses() !== null) { if ($customer->getId()) { @@ -368,8 +369,9 @@ private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInte * Populate customer model with secure data. * * @param \Magento\Framework\Api\CustomAttributesDataInterface $customer - * @param $passwordHash - * @param $customerModel + * @param string $passwordHash + * @param \Magento\Customer\Model\Customer\Interceptor $customerModel + * @return void */ private function populateCustomerModelWithSecureData( \Magento\Framework\Api\CustomAttributesDataInterface $customer, @@ -394,12 +396,16 @@ private function populateCustomerModelWithSecureData( /** * Set default billing. * - * @param $customerArr - * @param $prevCustomerDataArr - * @param $customerModel + * @param array $customerArr + * @param array $prevCustomerDataArr + * @param \Magento\Customer\Model\Customer\Interceptor $customerModel + * @return void */ - private function setDefaultBilling($customerArr, $prevCustomerDataArr, $customerModel) - { + private function setDefaultBilling( + $customerArr, + $prevCustomerDataArr, + $customerModel + ) { if (!array_key_exists('default_billing', $customerArr) && null !== $prevCustomerDataArr && array_key_exists('default_billing', $prevCustomerDataArr) @@ -411,12 +417,16 @@ private function setDefaultBilling($customerArr, $prevCustomerDataArr, $customer /** * Set default shipping. * - * @param $customerArr - * @param $prevCustomerDataArr - * @param $customerModel + * @param array $customerArr + * @param array $prevCustomerDataArr + * @param \Magento\Customer\Model\Customer\Interceptor $customerModel + * @return void */ - private function setDefaultShipping($customerArr, $prevCustomerDataArr, $customerModel) - { + private function setDefaultShipping( + $customerArr, + $prevCustomerDataArr, + $customerModel + ) { if (!array_key_exists('default_shipping', $customerArr) && null !== $prevCustomerDataArr && array_key_exists('default_shipping', $prevCustomerDataArr) From 3238dbd78fc71f2bb4e9bd2949dd5dae57fc9706 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Wed, 10 May 2017 13:06:27 +0300 Subject: [PATCH 062/363] MAGETWO-64297: No websites shown in grid --- .../Magento/Checkout/Test/Block/Cart/Totals.php | 1 + .../TestSuite/InjectableTests/MAGETWO-64297.xml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index e29a19a732409..7e1c3ac5ff599 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -105,6 +105,7 @@ class Totals extends Block */ public function getGrandTotal() { + $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); return $this->escapeCurrency($grandTotal); } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml new file mode 100644 index 0000000000000..0a292b70c0f19 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file From dab504bd8c437aaf349c64f0b06a83da06cbb476 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 14:38:13 +0300 Subject: [PATCH 063/363] MAGETWO-64243: Magento\Ui\Test\TestCase\GridSortingTest.test with data set "CmsBlocksGridSorting" --- .../Magento/Cms/Test/TestCase/GridSortingTest.xml | 11 +++++++++-- .../Customer/Test/TestCase/GridSortingTest.xml | 7 +++++++ .../TestSuite/InjectableTests/MAGETWO-64243.xml | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/GridSortingTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/GridSortingTest.xml index cf101cde54916..c02a109e75c90 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/GridSortingTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/GridSortingTest.xml @@ -10,9 +10,16 @@ to_maintain:yes Verify cms page grid sorting + 2 + cmsPage + default + + - + - + ID - Created + URL Key Magento\Cms\Test\Page\Adminhtml\CmsPageIndex getCmsPageGridBlock @@ -29,7 +36,7 @@ default ID - Created + Identifier Magento\Cms\Test\Page\Adminhtml\CmsBlockIndex getCmsBlockGrid diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/GridSortingTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/GridSortingTest.xml index 9640e19950b95..a1cb8d84ce8b4 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/GridSortingTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/GridSortingTest.xml @@ -10,10 +10,17 @@ to_maintain:yes Verify customer page grid sorting + 2 + customer + default ID Customer Since + + - + - + Magento\Customer\Test\Page\Adminhtml\CustomerIndex getCustomerGridBlock diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml new file mode 100644 index 0000000000000..efb40f03783eb --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 58148ee7d4643d4006fc0f63115eb52134a46511 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 15:55:45 +0300 Subject: [PATCH 064/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../Model/ResourceModel/CustomerRepository.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 81e2cdc2544f4..386e1259f86c5 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -131,8 +131,6 @@ public function __construct( } /** - * Save. - * * {@inheritdoc} */ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) @@ -207,8 +205,6 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa } /** - * Get. - * * {@inheritdoc} */ public function get($email, $websiteId = null) @@ -218,8 +214,6 @@ public function get($email, $websiteId = null) } /** - * Get by Id. - * * {@inheritdoc} */ public function getById($customerId) @@ -229,8 +223,6 @@ public function getById($customerId) } /** - * Get list. - * * {@inheritdoc} */ public function getList(SearchCriteriaInterface $searchCriteria) @@ -283,8 +275,6 @@ public function getList(SearchCriteriaInterface $searchCriteria) } /** - * Delete. - * * {@inheritdoc} */ public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer) @@ -293,8 +283,6 @@ public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer) } /** - * Delete by Id. - * * {@inheritdoc} */ public function deleteById($customerId) From d6661f12f53c2032b4b3f293b611045ca038acbe Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 10 May 2017 15:57:37 +0300 Subject: [PATCH 065/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Price/MinimalPriceCalculatorInterface.php | 32 +++ .../Price/MinimalTierPriceCalculator.php | 61 ++++++ .../Catalog/Pricing/Render/FinalPriceBox.php | 61 ++++-- .../Price/MinimalTierPriceCalculatorTest.php | 112 ++++++++++ .../Unit/Pricing/Render/FinalPriceBoxTest.php | 78 +++---- app/code/Magento/Catalog/etc/di.xml | 1 + .../Unit/Pricing/Render/FinalPriceBoxTest.php | 100 ++++----- .../Magento/Mtf/Util/Command/Cli/Indexer.php | 35 ++++ .../lib/Magento/Mtf/Util/Command/Website.php | 63 ++++++ .../Adminhtml/Product/FormPageActions.php | 43 ++++ .../Catalog/Test/Block/Product/Price.php | 13 ++ .../Test/Fixture/CatalogProductSimple.xml | 2 +- .../Test/Fixture/Product/WebsiteIds.php | 144 +++++++++++++ .../Handler/CatalogProductSimple/Curl.php | 9 +- .../Test/Repository/CatalogProductSimple.xml | 152 ++++++++++---- .../Catalog/Test/Repository/ConfigData.xml | 16 ++ .../Config/Test/Fixture/ConfigData.xml | 2 +- .../Test/Fixture/ConfigData/Section.php | 198 ++++++++++++++++++ .../Config/Test/Handler/ConfigData/Curl.php | 33 ++- .../AssertConfigurableProductInCategory.php | 7 + .../Test/Fixture/ConfigurableProduct.xml | 2 +- .../Test/Repository/ConfigurableProduct.xml | 84 ++++++-- .../ConfigurableAttributesData.xml | 32 +++ .../Repository/ConfigurableProduct/Price.xml | 4 + ...teConfigurableProductCustomWebsiteTest.php | 40 ++++ ...teConfigurableProductCustomWebsiteTest.xml | 18 ++ ...pdateSimplesInConfigurablePerStoreStep.php | 134 ++++++++++++ .../ConfigurableProduct/Test/etc/testcase.xml | 5 + .../Test/Repository/ConfigurableProduct.xml | 4 +- .../Store/Test/Handler/Website/Curl.php | 78 ++++++- .../Test/Repository/ConfigurableProduct.xml | 4 +- dev/tests/functional/utils/website.php | 32 +++ .../Pricing/Render/FinalPriceBoxTest.php | 145 +++++++++++++ .../_files/product_different_store_prices.php | 58 +++++ ...roduct_different_store_prices_rollback.php | 25 +++ .../product_has_tier_price_show_as_low_as.php | 75 +++++++ ...has_tier_price_show_as_low_as_rollback.php | 23 ++ 37 files changed, 1763 insertions(+), 162 deletions(-) create mode 100644 app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php create mode 100644 app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php create mode 100644 dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Indexer.php create mode 100644 dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/WebsiteIds.php create mode 100644 dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php create mode 100644 dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.php create mode 100644 dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.xml create mode 100644 dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestStep/UpdateSimplesInConfigurablePerStoreStep.php create mode 100644 dev/tests/functional/utils/website.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/FinalPriceBoxTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as_rollback.php diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php new file mode 100644 index 0000000000000..aeaf31a0c8a22 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php @@ -0,0 +1,32 @@ +calculator = $calculator; + } + + /** + * Get raw value of "as low as" as a minimal among tier prices + * {@inheritdoc} + */ + public function getValue(SaleableInterface $saleableItem) + { + /** @var TierPrice $price */ + $price = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); + $tierPriceList = $price->getTierPriceList(); + + $tierPrices = []; + foreach ($tierPriceList as $tierPrice) { + /** @var AmountInterface $price */ + $price = $tierPrice['price']; + $tierPrices[] = $price->getValue(); + } + + return $tierPrices ? min($tierPrices) : null; + } + + /** + * Return calculated amount object that keeps "as low as" value + * {@inheritdoc} + */ + public function getAmount(SaleableInterface $saleableItem) + { + $value = $this->getValue($saleableItem); + + return $value === null ? null : $this->calculator->getAmount($value, $saleableItem); + } +} diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index bab93e08f2753..8741dcf648aac 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -8,40 +8,41 @@ use Magento\Catalog\Pricing\Price; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Module\Manager; -use Magento\Framework\Pricing\Render; use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; -use Magento\Msrp\Pricing\Price\MsrpPrice; -use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; -use Magento\Framework\View\Element\Template\Context; use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Msrp\Pricing\Price\MsrpPrice; +use Magento\Framework\View\Element\Template\Context; /** * Class for final_price rendering * * @method bool getUseLinkForAsLowAs() * @method bool getDisplayMinimalPrice() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FinalPriceBox extends BasePriceBox { /** - * @var SalableResolverInterface + * @var \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface */ private $salableResolver; - /** @var Manager */ + /** @var \Magento\Framework\Module\Manager */ private $moduleManager; + /** + * @var \Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface + */ + private $minimalPriceCalculator; + /** * @param Context $context * @param SaleableInterface $saleableItem * @param PriceInterface $price * @param RendererPool $rendererPool * @param array $data - * @param SalableResolverInterface $salableResolver + * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface $salableResolver */ public function __construct( Context $context, @@ -49,11 +50,11 @@ public function __construct( PriceInterface $price, RendererPool $rendererPool, array $data = [], - SalableResolverInterface $salableResolver = null + \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface $salableResolver = null ) { parent::__construct($context, $saleableItem, $price, $rendererPool, $data); - $this->salableResolver = $salableResolver ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(SalableResolverInterface::class); + $this->salableResolver = $salableResolver ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface::class); } /** @@ -136,11 +137,15 @@ protected function wrapResult($html) */ public function renderAmountMinimal() { - /** @var \Magento\Catalog\Pricing\Price\FinalPrice $price */ - $price = $this->getPriceType(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); $id = $this->getPriceId() ? $this->getPriceId() : 'product-minimal-price-' . $this->getSaleableItem()->getId(); + $amount = $this->getMinimalPriceCalculator()->getAmount($this->getSaleableItem()); + + if ($amount === null) { + return ''; + } + return $this->renderAmount( - $price->getMinimalPrice(), + $amount, [ 'display_label' => __('As low as'), 'price_id' => $id, @@ -169,13 +174,15 @@ public function hasSpecialPrice() */ public function showMinimalPrice() { + $minTierPrice = $this->getMinimalPriceCalculator()->getValue($this->getSaleableItem()); + /** @var Price\FinalPrice $finalPrice */ $finalPrice = $this->getPriceType(Price\FinalPrice::PRICE_CODE); $finalPriceValue = $finalPrice->getAmount()->getValue(); - $minimalPriceAValue = $finalPrice->getMinimalPrice()->getValue(); + return $this->getDisplayMinimalPrice() - && $minimalPriceAValue - && $minimalPriceAValue < $finalPriceValue; + && $minTierPrice !== null + && $minTierPrice < $finalPriceValue; } /** @@ -203,12 +210,12 @@ public function getCacheKeyInfo() /** * @deprecated - * @return Manager + * @return \Magento\Framework\Module\Manager */ private function getModuleManager() { if ($this->moduleManager === null) { - $this->moduleManager = ObjectManager::getInstance()->get(Manager::class); + $this->moduleManager = ObjectManager::getInstance()->get(\Magento\Framework\Module\Manager::class); } return $this->moduleManager; } @@ -224,4 +231,18 @@ public function isProductList() $isProductList = $this->getData('is_product_list'); return $isProductList === true; } + + /** + * @deprecated + * @return \Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface + */ + private function getMinimalPriceCalculator() + { + if ($this->minimalPriceCalculator == null) { + $this->minimalPriceCalculator = ObjectManager::getInstance() + ->get(\Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface::class); + } + + return $this->minimalPriceCalculator; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php new file mode 100644 index 0000000000000..a649d3500c965 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php @@ -0,0 +1,112 @@ +price = $this->getMock(TierPrice::class, [], [], '', false); + $this->priceInfo = $this->getMockForAbstractClass(PriceInfoInterface::class); + $this->saleable = $this->getMockForAbstractClass(SaleableInterface::class); + + $this->objectManager = new ObjectManager($this); + + $this->calculator = $this->getMockForAbstractClass(CalculatorInterface::class); + $this->object = $this->objectManager->getObject( + MinimalTierPriceCalculator::class, + ['calculator' => $this->calculator] + ); + } + + private function getValueTierPricesExistShouldReturnMinTierPrice() + { + $minPrice = 5; + $notMinPrice = 10; + + $minAmount = $this->getMockForAbstractClass(AmountInterface::class); + $minAmount->expects($this->once())->method('getValue')->willReturn($minPrice); + + $notMinAmount = $this->getMockForAbstractClass(AmountInterface::class); + $notMinAmount->expects($this->once())->method('getValue')->willReturn($notMinPrice); + $tierPriceList = [ + [ + 'price' => $minAmount + ], + [ + 'price' => $notMinAmount + ] + ]; + + $this->price->expects($this->once())->method('getTierPriceList')->willReturn($tierPriceList); + + $this->priceInfo->expects($this->once())->method('getPrice')->with(TierPrice::PRICE_CODE) + ->willReturn($this->price); + + $this->saleable->expects($this->once())->method('getPriceInfo')->willReturn($this->priceInfo); + + return $minPrice; + } + + public function testGetValueTierPricesExistShouldReturnMinTierPrice() + { + $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); + $this->assertEquals($minPrice, $this->object->getValue($this->saleable)); + } + + public function testGetGetAmountMinTierPriceExistShouldReturnAmountObject() + { + $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->calculator->expects($this->once()) + ->method('getAmount') + ->with($minPrice, $this->saleable) + ->willReturn($amount); + + $this->assertSame($amount, $this->object->getAmount($this->saleable)); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index f2455661cf2d6..0b1690c8f73f1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -9,6 +9,10 @@ use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Framework\Module\Manager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Render\Amount; +use Magento\Catalog\Pricing\Price\FinalPrice; /** * Class FinalPriceBoxTest @@ -72,6 +76,11 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManager; + /** + * @var MinimalPriceCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $minimalPriceCalculator; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -168,7 +177,7 @@ protected function setUp() $this->price = $this->getMock(\Magento\Framework\Pricing\Price\PriceInterface::class); $this->price->expects($this->any()) ->method('getPriceCode') - ->will($this->returnValue(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)); + ->will($this->returnValue(FinalPrice::PRICE_CODE)); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -176,6 +185,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->minimalPriceCalculator =$this->getMockBuilder(MinimalPriceCalculatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->object = $this->objectManager->getObject( \Magento\Catalog\Pricing\Render\FinalPriceBox::class, [ @@ -184,7 +197,8 @@ protected function setUp() 'rendererPool' => $this->rendererPool, 'price' => $this->price, 'data' => ['zone' => 'test_zone', 'list_category_page' => true], - 'salableResolver' => $this->salableResolverMock + 'salableResolver' => $this->salableResolverMock, + 'minimalPriceCalculator' => $this->minimalPriceCalculator ] ); @@ -316,12 +330,18 @@ public function testRenderMsrpNotRegisteredException() public function testRenderAmountMinimal() { - $priceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); - $amount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); $priceId = 'price_id'; $html = 'html'; $this->object->setData('price_id', $priceId); + $this->product->expects($this->never())->method('getId'); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->minimalPriceCalculator->expects($this->once())->method('getAmount') + ->with($this->product) + ->willReturn($amount); + $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, @@ -331,24 +351,15 @@ public function testRenderAmountMinimal() 'skip_adjustments' => true, ]; - $amountRender = $this->getMock(\Magento\Framework\Pricing\Render\Amount::class, ['toHtml'], [], '', false); + $amountRender = $this->getMock(Amount::class, ['toHtml'], [], '', false); $amountRender->expects($this->once()) ->method('toHtml') - ->will($this->returnValue($html)); - - $this->priceInfo->expects($this->once()) - ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($priceType)); - - $priceType->expects($this->once()) - ->method('getMinimalPrice') - ->will($this->returnValue($amount)); + ->willReturn($html); $this->rendererPool->expects($this->once()) ->method('createAmountRender') ->with($amount, $this->product, $this->price, $arguments) - ->will($this->returnValue($amountRender)); + ->willReturn($amountRender); $this->assertEquals($html, $this->object->renderAmountMinimal()); } @@ -362,9 +373,9 @@ public function testRenderAmountMinimal() public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) { $regularPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\RegularPrice::class, [], [], '', false); - $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); - $regularPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); - $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + $regularPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $regularPriceAmount->expects($this->once()) ->method('getValue') @@ -386,7 +397,7 @@ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) ->will($this->returnValue($regularPriceType)); $this->priceInfo->expects($this->at(1)) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) + ->with(FinalPrice::PRICE_CODE) ->will($this->returnValue($finalPriceType)); $this->assertEquals($expectedResult, $this->object->hasSpecialPrice()); @@ -403,35 +414,30 @@ public function hasSpecialPriceProvider() public function testShowMinimalPrice() { - $finalPrice = 10.0; $minimalPrice = 5.0; - $displayMininmalPrice = 2.0; - - $this->object->setDisplayMinimalPrice($displayMininmalPrice); + $finalPrice = 10.0; + $displayMininmalPrice = true; - $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); + $this->minimalPriceCalculator->expects($this->once())->method('getValue')->with($this->product) + ->willReturn($minimalPrice); - $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); - $minimalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $finalPriceAmount->expects($this->once()) ->method('getValue') ->will($this->returnValue($finalPrice)); - $minimalPriceAmount->expects($this->once()) - ->method('getValue') - ->will($this->returnValue($minimalPrice)); - $finalPriceType->expects($this->at(0)) + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + $finalPriceType->expects($this->once()) ->method('getAmount') ->will($this->returnValue($finalPriceAmount)); - $finalPriceType->expects($this->at(1)) - ->method('getMinimalPrice') - ->will($this->returnValue($minimalPriceAmount)); $this->priceInfo->expects($this->once()) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($finalPriceType)); + ->with(FinalPrice::PRICE_CODE) + ->willReturn($finalPriceType); + + $this->object->setDisplayMinimalPrice($displayMininmalPrice); $this->assertTrue($this->object->showMinimalPrice()); } diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index ae1bc9c524c8e..44a65b6697d0c 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -807,4 +807,5 @@ + diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index 61f423f77cee8..006fce54465ac 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -9,6 +9,10 @@ use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Framework\Module\Manager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Render\Amount; +use Magento\Catalog\Pricing\Price\FinalPrice; /** * Class FinalPriceBoxTest @@ -72,6 +76,11 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManager; + /** + * @var MinimalPriceCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $minimalPriceCalculator; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -89,12 +98,12 @@ protected function setUp() ->method('getPriceInfo') ->will($this->returnValue($this->priceInfo)); - $eventManager = $this->getMock('Magento\Framework\Event\Test\Unit\ManagerStub', [], [], '', false); + $eventManager = $this->getMock(\Magento\Framework\Event\Test\Unit\ManagerStub::class, [], [], '', false); $config = $this->getMock('Magento\Store\Model\Store\Config', [], [], '', false); - $this->layout = $this->getMock('Magento\Framework\View\Layout', [], [], '', false); + $this->layout = $this->getMock(\Magento\Framework\View\Layout::class, [], [], '', false); - $this->priceBox = $this->getMock('Magento\Framework\Pricing\Render\PriceBox', [], [], '', false); - $this->logger = $this->getMock('Psr\Log\LoggerInterface'); + $this->priceBox = $this->getMock(\Magento\Framework\Pricing\Render\PriceBox::class, [], [], '', false); + $this->logger = $this->getMock(\Psr\Log\LoggerInterface::class); $this->layout->expects($this->any())->method('getBlock')->willReturn($this->priceBox); @@ -117,8 +126,8 @@ protected function setUp() ->getMockForAbstractClass(); $storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $scopeConfigMock = $this->getMockForAbstractClass('Magento\Framework\App\Config\ScopeConfigInterface'); - $context = $this->getMock('Magento\Framework\View\Element\Template\Context', [], [], '', false); + $scopeConfigMock = $this->getMockForAbstractClass(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $context = $this->getMock(\Magento\Framework\View\Element\Template\Context::class, [], [], '', false); $context->expects($this->any()) ->method('getEventManager') ->will($this->returnValue($eventManager)); @@ -150,11 +159,11 @@ protected function setUp() ->method('getUrlBuilder') ->will($this->returnValue($urlBuilder)); - $this->rendererPool = $this->getMockBuilder('Magento\Framework\Pricing\Render\RendererPool') + $this->rendererPool = $this->getMockBuilder(\Magento\Framework\Pricing\Render\RendererPool::class) ->disableOriginalConstructor() ->getMock(); - $this->price = $this->getMock('Magento\Framework\Pricing\Price\PriceInterface'); + $this->price = $this->getMock(\Magento\Framework\Pricing\Price\PriceInterface::class); $this->price->expects($this->any()) ->method('getPriceCode') ->will($this->returnValue(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)); @@ -165,15 +174,18 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->minimalPriceCalculator = $this->getMockForAbstractClass(MinimalPriceCalculatorInterface::class); + $this->object = $this->objectManager->getObject( - 'Magento\Catalog\Pricing\Render\FinalPriceBox', + \Magento\Catalog\Pricing\Render\FinalPriceBox::class, [ 'context' => $context, 'saleableItem' => $this->product, 'rendererPool' => $this->rendererPool, 'price' => $this->price, 'data' => ['zone' => 'test_zone', 'list_category_page' => true], - 'salableResolver' => $this->salableResolverMock + 'salableResolver' => $this->salableResolverMock, + 'minimalPriceCalculator' => $this->minimalPriceCalculator ] ); @@ -191,7 +203,7 @@ protected function setUp() public function testRenderMsrpDisabled() { - $priceType = $this->getMock('Magento\Msrp\Pricing\Price\MsrpPrice', [], [], '', false); + $priceType = $this->getMock(\Magento\Msrp\Pricing\Price\MsrpPrice::class, [], [], '', false); $this->moduleManager->expects(self::once()) ->method('isEnabled') @@ -223,7 +235,7 @@ public function testRenderMsrpDisabled() public function testRenderMsrpEnabled() { - $priceType = $this->getMock('Magento\Msrp\Pricing\Price\MsrpPrice', [], [], '', false); + $priceType = $this->getMock(\Magento\Msrp\Pricing\Price\MsrpPrice::class, [], [], '', false); $this->moduleManager->expects(self::once()) ->method('isEnabled') @@ -250,7 +262,7 @@ public function testRenderMsrpEnabled() ->with($this->equalTo($this->product)) ->will($this->returnValue(true)); - $priceBoxRender = $this->getMockBuilder('Magento\Framework\Pricing\Render\PriceBox') + $priceBoxRender = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class) ->disableOriginalConstructor() ->getMock(); $priceBoxRender->expects($this->once()) @@ -305,12 +317,19 @@ public function testRenderMsrpNotRegisteredException() public function testRenderAmountMinimal() { - $priceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); - $amount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); $priceId = 'price_id'; $html = 'html'; + $this->object->setData('price_id', $priceId); + $this->product->expects($this->never())->method('getId'); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->minimalPriceCalculator->expects($this->once())->method('getAmount') + ->with($this->product) + ->willReturn($amount); + $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, @@ -320,24 +339,15 @@ public function testRenderAmountMinimal() 'skip_adjustments' => true, ]; - $amountRender = $this->getMock('Magento\Framework\Pricing\Render\Amount', ['toHtml'], [], '', false); + $amountRender = $this->getMock(Amount::class, ['toHtml'], [], '', false); $amountRender->expects($this->once()) ->method('toHtml') - ->will($this->returnValue($html)); - - $this->priceInfo->expects($this->once()) - ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($priceType)); - - $priceType->expects($this->once()) - ->method('getMinimalPrice') - ->will($this->returnValue($amount)); + ->willReturn($html); $this->rendererPool->expects($this->once()) ->method('createAmountRender') ->with($amount, $this->product, $this->price, $arguments) - ->will($this->returnValue($amountRender)); + ->willReturn($amountRender); $this->assertEquals($html, $this->object->renderAmountMinimal()); } @@ -350,10 +360,10 @@ public function testRenderAmountMinimal() */ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) { - $regularPriceType = $this->getMock('Magento\Catalog\Pricing\Price\RegularPrice', [], [], '', false); - $finalPriceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); - $regularPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); - $finalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); + $regularPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\RegularPrice::class, [], [], '', false); + $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); + $regularPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); $regularPriceAmount->expects($this->once()) ->method('getValue') @@ -392,35 +402,31 @@ public function hasSpecialPriceProvider() public function testShowMinimalPrice() { - $finalPrice = 10.0; $minimalPrice = 5.0; - $displayMininmalPrice = 2.0; - - $this->object->setDisplayMinimalPrice($displayMininmalPrice); + $finalPrice = 10.0; + $displayMininmalPrice = true; - $finalPriceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); + $this->minimalPriceCalculator->expects($this->once())->method('getValue')->with($this->product) + ->willReturn($minimalPrice); - $finalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); - $minimalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $finalPriceAmount->expects($this->once()) ->method('getValue') ->will($this->returnValue($finalPrice)); - $minimalPriceAmount->expects($this->once()) - ->method('getValue') - ->will($this->returnValue($minimalPrice)); - $finalPriceType->expects($this->at(0)) + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + + $finalPriceType->expects($this->once()) ->method('getAmount') ->will($this->returnValue($finalPriceAmount)); - $finalPriceType->expects($this->at(1)) - ->method('getMinimalPrice') - ->will($this->returnValue($minimalPriceAmount)); $this->priceInfo->expects($this->once()) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($finalPriceType)); + ->with(FinalPrice::PRICE_CODE) + ->willReturn($finalPriceType); + + $this->object->setDisplayMinimalPrice($displayMininmalPrice); $this->assertTrue($this->object->showMinimalPrice()); } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Indexer.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Indexer.php new file mode 100644 index 0000000000000..0cf4f2bca24d5 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Indexer.php @@ -0,0 +1,35 @@ +transport = $transport; + } + + /** + * Creates Website folder in root directory. + * + * @param string $websiteCode + * @throws \Exception + */ + public function create($websiteCode) + { + $curl = $this->transport; + $curl->addOption(CURLOPT_HEADER, 1); + $curl->write($this->prepareUrl($websiteCode), [], CurlInterface::GET); + $curl->read(); + $curl->close(); + } + + /** + * Prepare url. + * + * @param string $websiteCode + * @return string + */ + private function prepareUrl($websiteCode) + { + return $_ENV['app_frontend_url'] . self::URL . '?website_code=' . urlencode($websiteCode); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php index 9935d12739068..b0a9ef2021d37 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/FormPageActions.php @@ -58,6 +58,27 @@ class FormPageActions extends ParentFormPageActions */ private $addAttribute = '[data-ui-id="addattribute-button"]'; + /** + * Default store switcher block locator. + * + * @var string + */ + private $storeSwitcherBlock = '.store-switcher'; + + /** + * Dropdown block locator. + * + * @var string + */ + private $dropdownBlock = '.dropdown'; + + /** + * Selector for confirm. + * + * @var string + */ + private $confirmModal = '.confirm._show[data-role=modal]'; + /** * Click on "Save" button. * @@ -122,4 +143,26 @@ public function addNewAttribute() { $this->_rootElement->find($this->addAttribute)->click(); } + + /** + * Change Store View scope. + * + * @param FixtureInterface $store + * @return void + */ + public function changeStoreViewScope(FixtureInterface $store) + { + $this->waitForElementNotVisible($this->spinner); + $this->waitForElementVisible($this->storeSwitcherBlock); + $this->_rootElement->find($this->storeSwitcherBlock) + ->find($this->dropdownBlock, Locator::SELECTOR_CSS, 'liselectstore') + ->setValue(sprintf('%s/%s', $store->getGroupId(), $store->getName())); + $modalElement = $this->browser->find($this->confirmModal); + /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ + $modal = $this->blockFactory->create( + \Magento\Ui\Test\Block\Adminhtml\Modal::class, + ['element' => $modalElement] + ); + $modal->acceptAlert(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/Price.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/Price.php index 729aa0ed0be66..7046c2e22b9dc 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/Price.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/Price.php @@ -40,6 +40,9 @@ class Price extends \Magento\Catalog\Test\Block\AbstractPriceBlock ], 'price_including_tax' => [ 'selector' => '.price-including-tax .price' + ], + 'minimal_price' => [ + 'selector' => '.minimal-price-link' ] ]; @@ -161,4 +164,14 @@ public function isOldPriceVisible() { return $this->getTypePriceElement('old_price')->isVisible(); } + + /** + * This method returns if the special price is visible. + * + * @return bool + */ + public function isMinimalPriceVisible() + { + return $this->getTypePriceElement('minimal_price')->isVisible(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml index fb34a7687c7e1..e1ed20c930b66 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml @@ -75,7 +75,7 @@ - + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/WebsiteIds.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/WebsiteIds.php new file mode 100644 index 0000000000000..ede8093ebd6f2 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/WebsiteIds.php @@ -0,0 +1,144 @@ +fixtureFactory = $fixtureFactory; + $this->params = $params; + $this->fixtureData = $data; + } + + /** + * Return prepared data set. + * + * @param string $key [optional] + * @return mixed + * @throws \Exception + */ + public function getData($key = null) + { + if (empty($this->fixtureData)) { + throw new \Exception("Data must be set"); + } + + foreach ($this->fixtureData as $dataset) { + if (is_array($dataset) && isset($dataset['websites'])) { + foreach ($dataset['websites'] as $website) { + $this->websites[] = $website; + } + } else { + $this->createStore($dataset); + } + } + + return parent::getData($key); + } + + /** + * Create store. + * + * @param array|object $dataset + * @return void + */ + private function createStore($dataset) + { + if ($dataset instanceof Store) { + $store = $dataset; + } elseif (is_array($dataset)) { + $store = isset($dataset['store']) ? $dataset['store'] : + (isset($dataset['dataset']) ? $this->fixtureFactory->createByCode('store', $dataset) : null); + } + if (isset($store)) { + $this->setWebsiteStoreData($store); + } + } + + /** + * Set website and store data. + * + * @param Store $store + * @return void + */ + private function setWebsiteStoreData(Store $store) + { + if (!$store->getStoreId()) { + $store->persist(); + } + $website = $store->getDataFieldConfig('group_id')['source'] + ->getStoreGroup()->getDataFieldConfig('website_id')['source']->getWebsite(); + $this->data[] = $website->getName(); + $this->websites[] = $website; + $this->stores[] = $store; + } + + /** + * Return stores. + * + * @return array + */ + public function getStores() + { + return $this->stores; + } + + /** + * Return website codes. + * + * @return array + */ + public function getWebsites() + { + return $this->websites; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php index 8087e6363c718..b66b4fec7a2b8 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php @@ -414,7 +414,14 @@ protected function prepareCategory() */ protected function prepareWebsites() { - if (!empty($this->fields['product']['website_ids'])) { + if (isset($this->fixture->getDataFieldConfig('website_ids')['source'])) { + $webSitesSource = $this->fixture->getDataFieldConfig('website_ids')['source']; + + foreach ($webSitesSource->getWebsites() as $key => $website) { + $this->fields['product']['website_ids'][$key] = $website->getWebsiteId(); + } + + } else { foreach ($this->fields['product']['website_ids'] as $key => $website) { $website = isset($this->mappingData['website_ids'][$website]) ? $this->mappingData['website_ids'][$website] diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 7a8a629ca765b..f2ce59cc091d2 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -26,7 +26,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -55,7 +57,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -84,7 +88,9 @@ default_subcategory - Main Website + + default + simple_order_default @@ -111,7 +117,9 @@ taxable_goods - Main Website + + default + Catalog, Search product-10-dollar-%isolation% @@ -139,7 +147,9 @@ taxable_goods - Main Website + + default + Catalog, Search product-20-dollar-%isolation% @@ -159,7 +169,9 @@ 560 - Main Website + + default + Catalog, Search @@ -186,7 +198,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -214,7 +228,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -239,7 +255,9 @@ 100 - Main Website + + default + simple-product-%isolation% @@ -266,7 +284,9 @@ product_40_dollar simple-product-%isolation% - Main Website + + default + @@ -291,7 +311,9 @@ simple_with_category - Main Website + + default + simple-product-%isolation% @@ -316,7 +338,9 @@ default_subcategory - Main Website + + default + simple_with_category simple-product-%isolation% @@ -340,7 +364,9 @@ In Stock
- Main Website + + default + @@ -361,7 +387,9 @@ This item has weight 100 - Main Website + + default + simple-product-%isolation% @@ -389,7 +417,9 @@ 1 Yes - Main Website + + default + Yes @@ -417,7 +447,9 @@ In Stock - Main Website + + default + simple-product-%isolation% @@ -446,7 +478,9 @@ default_subcategory - Main Website + + default + simple-product-%isolation% @@ -469,7 +503,9 @@ 9 - Main Website + + default + simple-product-%isolation% @@ -495,7 +531,9 @@ <p>dfj_full</p> Yes - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -519,7 +557,9 @@ <p>Simple with Weight 0.1</p> Yes - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -543,7 +583,9 @@ <p>Simple with Weight 150.1</p> Yes - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -568,7 +610,9 @@ Yes <p>abc_short</p> - Main Website + + default + Catalog, Search simple-product-%isolation% @@ -590,7 +634,9 @@ 100 - Main Website + + default + simple-product-%isolation% @@ -657,7 +703,9 @@ default_subcategory
- Main Website + + default + simple-product-%isolation% @@ -678,7 +726,9 @@ In Stock
- Main Website + + default + two_options @@ -711,7 +761,9 @@ simple_drop_down_with_one_option_percent_price - Main Website + + default + default_subcategory @@ -760,7 +812,9 @@ 100 - Main Website + + default + simple-product-%isolation% @@ -784,7 +838,9 @@ taxable_goods
- Main Website + + default + Catalog, Search @@ -812,7 +868,9 @@ taxable_goods - Main Website + + default + Catalog, Search @@ -841,7 +899,9 @@ taxable_goods - Main Website + + default + Not Visible Individually @@ -869,7 +929,9 @@ taxable_goods - Main Website + + default + Catalog, Search @@ -907,7 +969,9 @@ simple-product-%isolation% - Main Website + + default + @@ -930,7 +994,9 @@ taxable_goods
- Main Website + + default + Catalog, Search @@ -965,7 +1031,9 @@ more_is_cheaper - Main Website + + default + @@ -989,7 +1057,9 @@ default_anchor_subcategory
- Main Website + + default + simple_with_category simple-product-%isolation% @@ -1006,7 +1076,9 @@ taxable_goods
- Main Website + + default + Simple Product With Fpt %isolation% sku_simple_product_%isolation% @@ -1042,7 +1114,9 @@ taxable_goods
- Main Website + + default + Simple Product With Fpt %isolation% sku_simple_product_%isolation% @@ -1082,7 +1156,9 @@ taxable_goods
- Main Website + + default + Catalog, Search diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml index 093dc6043cdc1..48e1fff26ff3f 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml @@ -23,5 +23,21 @@ 0 + + + default + 0 + Website + 1 + + + + + default + 0 + Global + 0 + + diff --git a/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData.xml index 9f9174731a599..fa3338eb80170 100644 --- a/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData.xml @@ -14,7 +14,7 @@ repository_class="Magento\Config\Test\Repository\ConfigData" handler_interface="Magento\Config\Test\Handler\ConfigData\ConfigDataInterface" class="Magento\Config\Test\Fixture\ConfigData"> - + diff --git a/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php new file mode 100644 index 0000000000000..d8be9fe5baf0b --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php @@ -0,0 +1,198 @@ +fixtureFactory = $fixtureFactory; + $this->params = $params; + $this->fixtureData = $data; + } + + /** + * Return prepared data set. + * + * @param string $key [optional] + * @return mixed + */ + public function getData($key = null) + { + if ($this->data === null) { + if (isset($this->fixtureData['scope']['scope_type'])) { + $this->scopeData = $this->fixtureData['scope']; + $this->scopeType = $this->fixtureData['scope']['scope_type']; + $this->setLevel = $this->fixtureData['scope']['set_level']; + $this->prepareScopeData(); + unset($this->fixtureData['scope']); + } + $this->data = $this->replacePlaceholders($this->fixtureData); + } + + return parent::getData($key); + } + + /** + * Replace placeholders in parameters array. + * + * @param array $data + * @return array + */ + private function replacePlaceholders(array $data) + { + foreach ($data as &$params) { + $params = array_map(function ($value) { + if (is_string($value)) { + $value = str_replace( + '{{basic_url_to_secure}}', + preg_replace('/(http[s]?)/', 'https', $_ENV['app_frontend_url']), + $value + ); + $value = str_replace( + '{{basic_url_to_unsecure}}', + preg_replace('/(http[s]?)/', 'http', $_ENV['app_frontend_url']), + $value + ); + } + return $value; + }, $params); + } + return $data; + } + + /** + * Prepare scope data. + * + * @return void + * @throws \Exception + */ + private function prepareScopeData() + { + if (isset($this->scopeData['dataset'])) { + /** @var Store|Website $store */ + $this->scope = $this->fixtureFactory->createByCode( + $this->scopeType, + ['dataset' => $this->scopeData['dataset']] + ); + if (!$this->scope->hasData($this->scopeType . '_id')) { + $this->scope->persist(); + } + } elseif (isset($this->scopeData['fixture'])) { + $this->scope = $this->scopeData['fixture']; + } else { + throw new \Exception('Parameters "dataset" and "fixture" aren\'t identify.'); + } + + $this->prepareScope(); + } + + /** + * Prepare scope. + * + * @return void + * @throws \Exception + */ + private function prepareScope() + { + if ($this->setLevel == self::STORE_CODE && $this->scopeType == self::WEBSITE_CODE) { + throw new \Exception('Store level can\'t set to ["scope_type" = "website"].'); + } elseif ($this->setLevel == self::WEBSITE_CODE && $this->scopeType == self::STORE_CODE) { + $this->scopeType = $this->setLevel; + $this->scope = $this->scope + ->getDataFieldConfig('group_id')['source']->getStoreGroup() + ->getDataFieldConfig('website_id')['source']->getWebsite(); + } + } + + /** + * Return Store View or Website fixture. + * + * @return Store|Website + */ + public function getScope() + { + return $this->scope; + } + + /** + * Get get scope type [website|store]. + * + * @return string + */ + public function getScopeType() + { + return $this->scopeType; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php b/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php index c34f5951c03b3..50f11310abc3a 100644 --- a/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php @@ -10,6 +10,9 @@ use Magento\Mtf\Handler\Curl as AbstractCurl; use Magento\Mtf\Util\Protocol\CurlTransport; use Magento\Mtf\Util\Protocol\CurlTransport\BackendDecorator; +use Magento\Config\Test\Fixture\ConfigData\Section; +use Magento\Store\Test\Fixture\Store; +use Magento\Store\Test\Fixture\Website; /** * Setting config. @@ -29,6 +32,13 @@ class Curl extends AbstractCurl implements ConfigDataInterface ], ]; + /** + * FixtureInterface object. + * + * @var FixtureInterface + */ + private $fixture; + /** * Post request for setting configuration. * @@ -37,6 +47,7 @@ class Curl extends AbstractCurl implements ConfigDataInterface */ public function persist(FixtureInterface $fixture = null) { + $this->fixture = $fixture; $data = $this->prepareData($fixture); foreach ($data as $scope => $item) { $this->applyConfigSettings($item, $scope); @@ -132,6 +143,26 @@ protected function applyConfigSettings(array $data, $section) */ protected function getUrl($section) { - return $_ENV['app_backend_url'] . 'admin/system_config/save/section/' . $section; + return $_ENV['app_backend_url'] . 'admin/system_config/save/section/' . $section . $this->getStoreViewUrl(); + } + + /** + * Get store view url. + * + * @return string + */ + private function getStoreViewUrl() + { + $result = ''; + /** @var Section $source */ + $source = $this->fixture->getDataFieldConfig('section')['source']; + /** @var Store|Website $scope */ + $scope = $source->getScope(); + if ($scope !== null) { + $code = $source->getScopeType(); + $result = $code . '/' . $scope->getData($code . '_id'); + } + + return $result ? '/' . $result : ''; } } diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductInCategory.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductInCategory.php index 45bd430cb23eb..a6f9b9c48c4c8 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductInCategory.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductInCategory.php @@ -41,5 +41,12 @@ protected function assertPrice(FixtureInterface $product, CatalogCategoryView $c 'Product special price on category page is not correct.' ); } + + if (!$product->hasData('tier_price')) { + \PHPUnit_Framework_Assert::assertNotTrue( + $priceBlock->isMinimalPriceVisible(), + 'Minimal price block mustn\'t be visible on category page.' + ); + } } } diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct.xml index 70c613660d542..5964b0a66a976 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct.xml @@ -81,7 +81,7 @@ - + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml index 49a8fcd5a8bae..b57afacd2c308 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml @@ -29,7 +29,9 @@ In Stock - Main Website + + default + default @@ -60,7 +62,9 @@ In Stock - Main Website + + default + default @@ -92,7 +96,9 @@ In Stock - Main Website + + default + default @@ -120,7 +126,9 @@ In Stock - Main Website + + default + default @@ -152,7 +160,9 @@ In Stock - Main Website + + default + default @@ -184,7 +194,9 @@ In Stock - Main Website + + default + default @@ -214,7 +226,9 @@ default - Main Website + + default + default @@ -242,7 +256,9 @@ In Stock - Main Website + + default + default @@ -274,7 +290,9 @@ In Stock - Main Website + + default + default @@ -302,7 +320,9 @@ default_subcategory - Main Website + + default + two_options_with_fixed_price @@ -329,7 +349,9 @@ This item has weight 1 - Main Website + + default + two_variations_with_fixed_price @@ -365,7 +387,9 @@ In Stock - Main Website + + default + custom_attribute_set @@ -393,7 +417,9 @@ default_subcategory - Main Website + + default + two_options_by_one_dollar @@ -409,5 +435,37 @@ price_40 + + + Configurable product %isolation% + test-configurable-product-%isolation% + sku_configurable_product_%isolation% + + taxable_goods + + This item has weight + 1 + + + default + + + custom_store + + + + default_subcategory + + + two_options_ten_dollars + + + custom_attribute_set + + + 15 + price_15 + + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml index 66a4748e71288..09899f446a76d 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml @@ -662,5 +662,37 @@ + + + + + + + option_key_1_%isolation% + 10 + Yes + + + option_key_2_%isolation% + 10 + Yes + + + + + + catalogProductAttribute::attribute_type_dropdown_two_options + + + + 10 + 1 + + + 20 + 2 + + + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml index ae055f628d907..74de6f99ade2c 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml @@ -17,5 +17,9 @@ 11 + + 15 + 15 + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.php new file mode 100644 index 0000000000000..286b30d4510d1 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.php @@ -0,0 +1,40 @@ + Configuration > Catalog > Catalog > Price) + * 2. Create Additional Website, Store, Store View. + * 3. Create Configurable product with two variations, price for all variations is 10$ and assign it to both websites. + * 4. Open all simple products, which are assigned to configurable and change their price in Default Store View to 15$ + * 5. Do reindex and clear magento cache + * 6. Open on storefront on main website category with configurable product + * 7. "As low as" price not shown for configurable product + * + * @group Configurable_Product_(MX) + * @ZephyrId MAGETWO-64720 + */ +class UpdateConfigurableProductCustomWebsiteTest extends Scenario +{ + /* tags */ + const MVP = 'yes'; + const DOMAIN = 'MX'; + /* end tags */ + + /** + * Test update Configurable product with custom website run. + * + * @return void + */ + public function test() + { + $this->executeScenario(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.xml new file mode 100644 index 0000000000000..ca9456f7d6445 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductCustomWebsiteTest.xml @@ -0,0 +1,18 @@ + + + + + + price_per_website + configurableProduct::two_variations_two_websites + default + 15.00 + + + + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestStep/UpdateSimplesInConfigurablePerStoreStep.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestStep/UpdateSimplesInConfigurablePerStoreStep.php new file mode 100644 index 0000000000000..4e7e0c4b61d83 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestStep/UpdateSimplesInConfigurablePerStoreStep.php @@ -0,0 +1,134 @@ +indexer = $indexer; + $this->cache = $cache; + $this->store = $store; + $this->catalogProductEdit = $catalogProductEdit; + $this->productGrid = $productGrid; + $this->product = $product; + $this->updatedSimple = $updatedSimple; + } + + /** + * Update simple products data in configurable for store. + * + * @return void + */ + public function run() + { + if (!$this->store) { + return; + } + + $simpleProductsArray = []; + $configurableAttributes = $this->product->getConfigurableAttributesData(); + + if (isset($configurableAttributes['matrix'])) { + foreach ($configurableAttributes['matrix'] as $matrixItem) { + $simpleProductsArray[] = $matrixItem['sku']; + } + } + + foreach ($simpleProductsArray as $simpleProductSku) { + //open product + $filter = ['sku' => $simpleProductSku]; + $this->productGrid->open(); + $this->productGrid->getProductGrid()->searchAndOpen($filter); + //update + $this->catalogProductEdit->getFormPageActions()->changeStoreViewScope($this->store); + $this->catalogProductEdit->getProductForm()->fill($this->updatedSimple); + $this->catalogProductEdit->getFormPageActions()->save(); + } + + $this->indexer->reindex(); + $this->cache->flush(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/etc/testcase.xml index 837aeb382680c..f114a565bd394 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/etc/testcase.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/etc/testcase.xml @@ -13,4 +13,9 @@ + + + + + diff --git a/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/ConfigurableProduct.xml index b76ac44f92dd4..6225acda86698 100644 --- a/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/ConfigurableProduct.xml @@ -30,7 +30,9 @@ one_variation_one_dollar - Main Website + + default + custom_attribute_set diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php index eee1eb7b5815a..9f53fac6e4441 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php @@ -11,6 +11,11 @@ use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; use Magento\Mtf\Util\Protocol\CurlTransport\BackendDecorator; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\Mtf\Fixture\FixtureFactory; +use Magento\Mtf\Config\DataInterface; +use Magento\Mtf\System\Event\EventManagerInterface; +use Magento\Mtf\Util\Command\Website; /** * Class Curl @@ -18,6 +23,45 @@ */ class Curl extends AbstractCurl implements WebsiteInterface { + /** + * Website folder creation class instance. + * + * @var Website + */ + private $website; + + /** + * Website fixture. + * + * @var WebsiteFixture + */ + private $fixture; + + /** + * Fixture factory. + * + * @var FixtureFactory + */ + private $fixtureFactory; + + /** + * @constructor + * @param DataInterface $configuration + * @param EventManagerInterface $eventManager + * @param Website $website + * @param FixtureFactory $fixtureFactory + */ + public function __construct( + DataInterface $configuration, + EventManagerInterface $eventManager, + Website $website, + FixtureFactory $fixtureFactory + ) { + parent::__construct($configuration, $eventManager); + $this->website = $website; + $this->fixtureFactory = $fixtureFactory; + } + /** * POST request for creating Website * @@ -37,7 +81,19 @@ public function persist(FixtureInterface $fixture = null) throw new \Exception("Website entity creating by curl handler was not successful! Response: $response"); } - return ['website_id' => $this->getWebSiteIdByWebsiteName($fixture->getName())]; + $websiteId = $this->getWebSiteIdByWebsiteName($fixture->getName()); + + // Update website fixture data. + $this->fixture = $this->fixtureFactory->createByCode( + 'website', + ['data' => array_merge($fixture->getData(), ['website_id' => $websiteId])] + ); + $data['website']['website_id'] = $websiteId; + // Creates Website folder in root directory. + $this->website->create($data['website']['code']); + $this->setConfiguration($data); + + return ['website_id' => $websiteId]; } /** @@ -85,4 +141,24 @@ protected function prepareData(FixtureInterface $fixture) return $data; } + + /** + * Set Website configuration Base url. + * + * @param array $data + * @return void + * @throws \Exception + */ + private function setConfiguration(array $data) + { + $configData = [ + 'web/unsecure/base_link_url' => [ + 'value' => '{{unsecure_base_url}}websites/' . $data['website']['code'] . '/', + ], + 'scope' => ['fixture' => $this->fixture, 'scope_type' => 'website', 'set_level' => 'website'] + ]; + + $configFixture = $this->fixtureFactory->createByCode('configData', ['data' => $configData]); + $configFixture->persist(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Swatches/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/Swatches/Test/Repository/ConfigurableProduct.xml index 098f28965d65d..175209ab36c4a 100644 --- a/dev/tests/functional/tests/app/Magento/Swatches/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Swatches/Test/Repository/ConfigurableProduct.xml @@ -32,7 +32,9 @@ default_subcategory - Main Website + + default + custom_attribute_set diff --git a/dev/tests/functional/utils/website.php b/dev/tests/functional/utils/website.php new file mode 100644 index 0000000000000..0ed76714e1bab --- /dev/null +++ b/dev/tests/functional/utils/website.php @@ -0,0 +1,32 @@ +objectManager = Bootstrap::getObjectManager(); + + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + + $this->appState = $this->objectManager->get(State::class); + $this->appState->setAreaCode(Area::AREA_FRONTEND); + + $this->phtml = $this->objectManager->create(Php::class); + + $this->templateEnginePool = $this->objectManager->get(TemplateEnginePool::class); + + $enginesReflection = new \ReflectionProperty( + $this->templateEnginePool, + 'engines' + ); + $enginesReflection->setAccessible(true); + $enginesReflection->setValue($this->templateEnginePool, ['phtml' => $this->phtml]); + + $this->rendererPool = $this->objectManager->create(RendererPool::class); + + $this->rendererPool->setData( + [ + 'default' => + [ + 'default_amount_render_class' => Amount::class, + 'default_amount_render_template' => 'Magento_Catalog::product/price/amount/default.phtml' + ] + ] + ); + + $this->saleableItem = $this->productRepository->get('tier_prices'); + $this->finalPrice = $this->objectManager->create( + FinalPrice::class, + [ + 'saleableItem' => $this->saleableItem, + 'quantity' => null + ] + ); + + $this->finalPriceBox = $this->objectManager->create( + FinalPriceBox::class, + [ + 'saleableItem' => $this->saleableItem, + 'price' => $this->finalPrice, + 'rendererPool' => $this->rendererPool + ] + ); + + $this->finalPriceBox->setData('price_id', 'test_price_id'); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testRenderAmountMinimalProductWithTierPricesShouldShowMinTierPrice() + { + $result = $this->finalPriceBox->renderAmountMinimal(); + $this->assertContains('$5.00', $result); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_different_store_prices.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + */ + public function testProductSetDifferentStorePricesWithoutTierPriceShouldNotShowAsLowAs() + { + $this->assertEmpty($this->finalPriceBox->renderAmountMinimal()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices.php new file mode 100644 index 0000000000000..be0e890742102 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices.php @@ -0,0 +1,58 @@ +create(IndexerRegistry::class); +$indexer = $indexerRegistry->get('catalogsearch_fulltext'); + +$indexer->reindexAll(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('tier_prices') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +$product->setStoreId($store->getId()); +$product->setPrice(15); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices_rollback.php new file mode 100644 index 0000000000000..4c4be750e7a5d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_different_store_prices_rollback.php @@ -0,0 +1,25 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()-> +create(\Magento\Catalog\Model\ProductRepository::class); +try { + $product = $repository->get('tier_prices'); + $product->delete(); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/core_fixturestore_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php new file mode 100644 index 0000000000000..a5fad2e96e4a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php @@ -0,0 +1,75 @@ +reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$tierPrices = []; +/** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2, + 'value' => 8 + ] + ] +); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 5, + 'value' => 5 + ] + ] +); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3, + 'value' => 5 + ] + ] +); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('tier_prices') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setTierPrices($tierPrices) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->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' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepositoryFactory = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepositoryFactory->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as_rollback.php new file mode 100644 index 0000000000000..310b5445e9718 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_has_tier_price_show_as_low_as_rollback.php @@ -0,0 +1,23 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\ProductRepository::class); +try { + $product = $repository->get('tier_prices'); + $product->delete(); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 624a245bea1df0fba14205791224e6c4d14b7072 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 10 May 2017 16:37:02 +0300 Subject: [PATCH 066/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../Customer/Model/ResourceModel/CustomerRepositoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index fb7f2720299be..d812175a6db48 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -76,7 +76,7 @@ protected function tearDown() } /** - * Test create new customer new then update first name. + * Test create new customer, then update first name. * * @magentoDbIsolation enabled */ From 31cc24ca04090ac4c240ca98d12a091af16a3a54 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 11 May 2017 09:23:47 +0300 Subject: [PATCH 067/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Bundle/Test/Fixture/BundleProduct.xml | 2 +- .../Bundle/Test/Repository/BundleProduct.xml | 28 ++++++++++++++----- .../Test/Fixture/CatalogProductVirtual.xml | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/BundleProduct.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/BundleProduct.xml index be4c15784208b..7efc2bf12371b 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/BundleProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/BundleProduct.xml @@ -90,7 +90,7 @@ - + diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct.xml index e4c57688bbd66..e6a6abdc338bd 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct.xml @@ -17,7 +17,9 @@ bundle_dynamic_with_category - Main Website + + default + default_dynamic @@ -44,7 +46,9 @@ taxable_goods - Main Website + + default + Yes @@ -83,7 +87,9 @@ Yes Together - Main Website + + default + Yes @@ -117,7 +123,9 @@ 1 No - Main Website + + default + default_subcategory @@ -137,7 +145,9 @@ bundle_dynamic_with_category - Main Website + + default + default_subcategory @@ -162,7 +172,9 @@ 1 No - Main Website + + default + Separately @@ -193,7 +205,9 @@ Yes Together - Main Website + + default + Yes diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductVirtual.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductVirtual.xml index c113aff5e0d9d..be34fe83d87a7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductVirtual.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductVirtual.xml @@ -78,7 +78,7 @@ - + From c505683d4da133c28e8288e87ea6eb2d739f53b9 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 11 May 2017 10:56:06 +0300 Subject: [PATCH 068/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Test/Repository/CatalogProductVirtual.xml | 28 ++++++++++++++----- .../Test/Fixture/DownloadableProduct.xml | 2 +- .../Test/Repository/DownloadableProduct.xml | 24 ++++++++++++---- .../Test/Fixture/GroupedProduct.xml | 2 +- .../Test/Repository/GroupedProduct.xml | 16 ++++++++--- .../Test/Repository/CatalogProductSimple.xml | 8 ++++-- .../Test/Repository/CatalogProductSimple.xml | 8 ++++-- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductVirtual.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductVirtual.xml index fb564aa36328a..c485e1d3b9669 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductVirtual.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductVirtual.xml @@ -13,7 +13,9 @@ Yes - Main Website + + default + virtual-product%isolation% Catalog, Search @@ -47,7 +49,9 @@ This item has no weight - Main Website + + default + @@ -65,7 +69,9 @@ taxable_goods - Main Website + + default + @@ -75,7 +81,9 @@ Yes - Main Website + + default + virtual-product%isolation% Catalog, Search @@ -102,7 +110,9 @@ Yes - Main Website + + default + virtual-product%isolation% Catalog, Search @@ -131,7 +141,9 @@ Yes - Main Website + + default + virtual-product%isolation% Catalog, Search @@ -155,7 +167,9 @@ taxable_goods - Main Website + + default + virtual-product%isolation% Virtual product %isolation% diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Fixture/DownloadableProduct.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Fixture/DownloadableProduct.xml index f8d16a9de0121..c7873aeb46994 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Fixture/DownloadableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Fixture/DownloadableProduct.xml @@ -86,7 +86,7 @@ - + diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Repository/DownloadableProduct.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Repository/DownloadableProduct.xml index 42680d9f9377b..848401b881e45 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Repository/DownloadableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Repository/DownloadableProduct.xml @@ -30,7 +30,9 @@ This item has no weight - Main Website + + default + downloadable_default @@ -54,7 +56,9 @@ Yes Catalog, Search - Main Website + + default + with_two_separately_links @@ -81,7 +85,9 @@ Yes Catalog, Search - Main Website + + default + with_two_separately_links @@ -112,7 +118,9 @@ Catalog, Search - Main Website + + default + with_two_separately_links @@ -142,7 +150,9 @@ Catalog, Search - Main Website + + default + with_two_separately_links @@ -176,7 +186,9 @@ taxable_goods - Main Website + + default + downloadable_one_dollar_product_with_no_separated_link diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml index 4a0660e4d991f..0b934136929c1 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml @@ -70,7 +70,7 @@ - + diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml index c4ba219dfe4d3..22add5e3df058 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Repository/GroupedProduct.xml @@ -26,7 +26,9 @@ In Stock - Main Website + + default + default @@ -52,7 +54,9 @@ Out of Stock - Main Website + + default + default @@ -82,7 +86,9 @@ In Stock - Main Website + + default + default @@ -108,7 +114,9 @@ In Stock - Main Website + + default + default diff --git a/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/CatalogProductSimple.xml index 097187e1454ff..e8e6b987ef655 100644 --- a/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Msrp/Test/Repository/CatalogProductSimple.xml @@ -27,7 +27,9 @@ taxable_goods - Main Website + + default + Catalog, Search @@ -58,7 +60,9 @@ taxable_goods - Main Website + + default + No diff --git a/dev/tests/functional/tests/app/Magento/ProductVideo/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/ProductVideo/Test/Repository/CatalogProductSimple.xml index e822bf08044d5..8864204eab354 100755 --- a/dev/tests/functional/tests/app/Magento/ProductVideo/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/ProductVideo/Test/Repository/CatalogProductSimple.xml @@ -26,7 +26,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-with-video-%isolation% @@ -61,7 +63,9 @@ taxable_goods - Main Website + + default + Catalog, Search simple-product-with-video-%isolation% From cd5d0bbf4ae65a634cebfd97efa848ac5fb02276 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 11 May 2017 13:41:01 +0300 Subject: [PATCH 069/363] MAGETWO-63514: [Backport] - Free Shipping promo still applies for UPS after removing free ship item - for 2.1 --- .../Model/Quote/Address/Total/Shipping.php | 15 +-- .../Test/Repository/CatalogProductSimple.xml | 28 +++++ .../Checkout/Test/Block/Cart/Totals.php | 1 + .../SalesRule/Test/Handler/SalesRule/Curl.php | 28 +++-- .../SalesRule/Test/Repository/SalesRule.xml | 20 ++++ .../ShoppingCartWithFreeShippingTest.php | 79 ++++++++++++ .../ShoppingCartWithFreeShippingTest.xml | 25 ++++ .../Quote/Address/Total/ShippingTest.php | 113 ++++++++++++++++++ .../_files/cart_rule_free_shipping.php | 74 ++++++++++++ .../cart_rule_free_shipping_rollback.php | 17 +++ .../rule_free_shipping_by_product_weight.php | 36 ++++++ ...ee_shipping_by_product_weight_rollback.php | 7 ++ 12 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 289dc05c365ba..4b91466195eb9 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -59,11 +59,8 @@ public function collect( $addressWeight = $address->getWeight(); $freeMethodWeight = $address->getFreeMethodWeight(); + $addressFreeShipping = $address->getFreeShipping(); - $isAllFree = $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()); - if ($isAllFree && !$address->getFreeShipping()) { - $address->setFreeShipping(true); - } $total->setTotalAmount($this->getCode(), 0); $total->setBaseTotalAmount($this->getCode(), 0); @@ -99,7 +96,7 @@ public function collect( $itemQty = $child->getTotalQty(); $rowWeight = $itemWeight * $itemQty; $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $child->getFreeShipping() === true) { + if ($addressFreeShipping || $child->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($child->getFreeShipping())) { $freeQty = $child->getFreeShipping(); @@ -117,7 +114,7 @@ public function collect( $itemWeight = $item->getWeight(); $rowWeight = $itemWeight * $item->getQty(); $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $item->getFreeShipping() === true) { + if ($addressFreeShipping || $item->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($item->getFreeShipping())) { $freeQty = $item->getFreeShipping(); @@ -137,7 +134,7 @@ public function collect( $itemWeight = $item->getWeight(); $rowWeight = $itemWeight * $item->getQty(); $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $item->getFreeShipping() === true) { + if ($addressFreeShipping || $item->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($item->getFreeShipping())) { $freeQty = $item->getFreeShipping(); @@ -158,6 +155,10 @@ public function collect( $address->setWeight($addressWeight); $address->setFreeMethodWeight($freeMethodWeight); + $address->setFreeShipping( + $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()) + ); + $address->collectShippingRates(); if ($method) { diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 7a8a629ca765b..e441da8d2b1ed 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1103,5 +1103,33 @@ overnight-duffle + + + default + + Simple Product %isolation% + sku_simple_product_%isolation% + This item has weight + 10 + + 25 + In Stock + + + 560 + + + taxable_goods + + + Main Website + + Catalog, Search + simple-product-%isolation% + + simple_order_default + + + diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index e29a19a732409..7e1c3ac5ff599 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -105,6 +105,7 @@ class Totals extends Block */ public function getGrandTotal() { + $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); return $this->escapeCurrency($grandTotal); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php index 9b8aa37304e04..404961d085d43 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php @@ -38,52 +38,56 @@ class Curl extends Conditions implements SalesRuleInterface */ protected $mapTypeParams = [ 'Subtotal' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'base_subtotal', ], 'Total Items Quantity' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'total_qty', ], 'Conditions combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Combine', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, 'aggregator' => 'all', 'value' => '1', ], 'Products subselection' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Subselect', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'aggregator' => 'all', ], 'Product attribute combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Found', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, 'value' => '1', 'aggregator' => 'all', ], 'Shipping Country' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'country_id', ], 'Shipping Postcode' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'postcode', ], + 'Total Weight' => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + ], 'Category' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'category_ids', ], 'Price in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_price', ], 'Quantity in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_qty', ], 'Row total in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_row_total', ] ]; @@ -207,7 +211,7 @@ public function prepareData(FixtureInterface $fixture) if (isset($this->data['actions_serialized'])) { $this->mapTypeParams['Conditions combination']['type'] = - 'Magento\SalesRule\Model\Rule\Condition\Product\Combine'; + \Magento\SalesRule\Model\Rule\Condition\Product\Combine::class; $this->data['rule']['actions'] = $this->prepareCondition($this->data['actions_serialized']); unset($this->data['actions_serialized']); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 13479dbfc19e4..98c130ef0d37e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -279,5 +279,25 @@ No No + + + Cart price rule with free shipping by weight + Yes + + Main Website + + + NOT LOGGED IN + + No Coupon + 1 + Yes + [Total Weight|is|1] + Percent of product price discount + 0 + No + No + For matching items only + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php new file mode 100644 index 0000000000000..02a7d90db5e5e --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php @@ -0,0 +1,79 @@ +testStepFactory = $testStepFactory; + } + + /** + * Test sales rule with free shipping applied by product weight. + * + * @param \Magento\SalesRule\Test\Fixture\SalesRule $salesRule + * @param \Magento\Catalog\Test\Fixture\CatalogProductSimple $product + * @param \Magento\Checkout\Test\Fixture\Cart $cart + * @return void + */ + public function testRuleWithFreeShippingByWeight( + \Magento\SalesRule\Test\Fixture\SalesRule $salesRule, + \Magento\Catalog\Test\Fixture\CatalogProductSimple $product, + \Magento\Checkout\Test\Fixture\Cart $cart + ) { + $salesRule->persist(); + $product->persist(); + + $this->testStepFactory->create( + \Magento\Checkout\Test\TestStep\AddProductsToTheCartStep::class, + ['products' => [$product]] + )->run(); + + $this->testStepFactory->create( + \Magento\Checkout\Test\TestStep\EstimateShippingAndTaxStep::class, + ['products' => [$product], 'cart' => $cart] + )->run(); + } + + /** + * Clear data after test. + * + * @return void + */ + public function tearDown() + { + $this->testStepFactory->create(\Magento\SalesRule\Test\TestStep\DeleteAllSalesRuleStep::class)->run(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml new file mode 100644 index 0000000000000..b9c611a08cd14 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml @@ -0,0 +1,25 @@ + + + + + + rule_with_freeshipping_by_weight + default + 560.00 + 0.00 + 560.00 + + + rule_with_freeshipping_by_weight + simple_with_weight_10_for_salesrule + 560.00 + 5.00 + 565.00 + + + diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php new file mode 100644 index 0000000000000..8e8466ab13717 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php @@ -0,0 +1,113 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->cartManagement = $this->objectManager->get(\Magento\Quote\Api\GuestCartManagementInterface::class); + $this->itemRepository = $this->objectManager->get(\Magento\Quote\Api\GuestCartItemRepositoryInterface::class); + } + + /** + * Estimate shipment for product that match salesrule with free shipping. + * + * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testRuleByProductWeightWithFreeShipping() + { + $cartId = $this->prepareQuote(1); + $methods = $this->estimateShipping($cartId); + + $this->assertTrue(count($methods) > 0); + $this->assertEquals('flatrate', $methods[0]->getMethodCode()); + $this->assertEquals(0, $methods[0]->getAmount()); + + } + + /** + * Estimate shipment for product that doesn't match salesrule with free shipping. + * + * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testRuleByProductWeightWithoutFreeShipping() + { + $cartId = $this->prepareQuote(5); + $methods = $this->estimateShipping($cartId); + + $this->assertTrue(count($methods) > 0); + $this->assertEquals('flatrate', $methods[0]->getMethodCode()); + $this->assertEquals(25, $methods[0]->getAmount()); + + } + + /** + * Estimate shipment for guest cart. + * + * @param int $cartId + * @return \Magento\Quote\Api\Data\ShippingMethodInterface[] + */ + private function estimateShipping($cartId) + { + $addressFactory = $this->objectManager->get(\Magento\Quote\Api\Data\AddressInterfaceFactory::class); + /** @var \Magento\Quote\Api\Data\AddressInterface $address */ + $address = $addressFactory->create(); + $address->setCountryId('US'); + $address->setRegionId(2); + + /** @var \Magento\Quote\Api\GuestShipmentEstimationInterface $estimation */ + $estimation = $this->objectManager->get(\Magento\Quote\Api\GuestShipmentEstimationInterface::class); + return $estimation->estimateByExtendedAddress($cartId, $address); + } + + /** + * Create guest quote with products. + * + * @param int $itemQty + * @return int + */ + private function prepareQuote($itemQty) + { + $cartId = $this->cartManagement->createEmptyCart(); + + /** @var \Magento\Quote\Api\Data\CartItemInterfaceFactory $cartItemFactory */ + $cartItemFactory = $this->objectManager->get(\Magento\Quote\Api\Data\CartItemInterfaceFactory::class); + + /** @var \Magento\Quote\Api\Data\CartItemInterface $cartItem */ + $cartItem = $cartItemFactory->create(); + $cartItem->setQuoteId($cartId); + $cartItem->setQty($itemQty); + $cartItem->setSku('simple'); + $cartItem->setProductType(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE); + + $this->itemRepository->save($cartItem); + + return $cartId; + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php new file mode 100644 index 0000000000000..dc0b6e6f834c3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php @@ -0,0 +1,74 @@ +create(\Magento\SalesRule\Model\RuleFactory::class); +/** @var \Magento\SalesRule\Model\Rule $salesRule */ +$salesRule = $salesRuleFactory->create(); +$row = + [ + 'name' => 'Free shipping if item price >10', + 'is_active' => 1, + 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [ + 1 => + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + ] + + ], + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'quote_item_price', + 'operator' => '==', + 'value' => '7', + 'is_value_processed' => false, + ] + ] + ] + ], + 'is_advanced' => 1, + 'simple_action' => 'by_percent', + 'discount_amount' => 0, + 'stop_rules_processing' => 0, + 'discount_qty' => 0, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'times_used' => 0, + 'is_rss' => 1, + 'use_auto_generation' => 0, + 'uses_per_coupon' => 0, + 'simple_free_shipping' => 1, + + 'website_ids' => [ + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getId() + ] + ]; +$salesRule->loadPost($row); +$salesRule->save(); +/** @var Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('cart_rule_free_shipping'); +$registry->register('cart_rule_free_shipping', $salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php new file mode 100644 index 0000000000000..48471841b79ba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php @@ -0,0 +1,17 @@ +get(\Magento\Framework\Registry::class); + +/** @var \Magento\SalesRule\Model\Rule $salesRule */ +$salesRule = $registry->registry('cart_rule_free_shipping'); +if ($salesRule !== null) { + $salesRule->delete(); + $registry->unregister('cart_rule_free_shipping'); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php new file mode 100644 index 0000000000000..8dfef696b4fec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php @@ -0,0 +1,36 @@ + 'Free shipping if item weight <= 1', + 'conditions' => [ + 1 => + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + 'operator' => '<=', + 'value' => '1', + 'is_value_processed' => false, + ] + ] + ] + + ], + 'actions' => [], + ]; +$salesRule->loadPost($row); +$salesRule->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php new file mode 100644 index 0000000000000..c387fd35650c3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php @@ -0,0 +1,7 @@ + Date: Thu, 11 May 2017 17:08:41 +0300 Subject: [PATCH 070/363] MAGETWO-63587: [Backport] - Use default URL Key - for 2.1 --- .../CategoryUrlPathAutogeneratorObserver.php | 3 +- ...tegoryUrlPathAutogeneratorObserverTest.php | 48 +++++++++++++------ .../Adminhtml/Category/Edit/CategoryForm.php | 7 ++- .../Adminhtml/Category/Edit/CategoryForm.xml | 6 ++- .../Adminhtml/Category/Edit/PageActions.php | 39 +++++++++++++++ .../Test/Constraint/AssertCategoryForm.php | 8 +++- .../Magento/Catalog/Test/Fixture/Category.xml | 3 +- .../Catalog/Test/Repository/Category.xml | 11 +++++ .../Category/UpdateCategoryEntityTest.php | 14 +++--- .../Category/UpdateCategoryEntityTest.xml | 9 ++++ 10 files changed, 121 insertions(+), 27 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index 206ad612900a9..93892eb8156d8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -47,7 +47,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ $category = $observer->getEvent()->getCategory(); - if ($category->getUrlKey() !== false) { + $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); + if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $category->setUrlKey($this->categoryUrlPathGenerator->getUrlKey($category)) ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); if (!$category->isObjectNew()) { diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index a6fdc41cd11ee..db153f6cb0a4a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -37,16 +37,34 @@ class CategoryUrlPathAutogeneratorObserverTest extends \PHPUnit_Framework_TestCa protected function setUp() { $this->observer = $this->getMock( - 'Magento\Framework\Event\Observer', - ['getEvent', 'getCategory'], + \Magento\Framework\Event\Observer::class, + [ + 'getEvent', + 'getCategory' + ], + [], + '', + false + ); + $this->categoryResource = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Category::class, + [], [], '', false ); - $this->categoryResource = $this->getMock('Magento\Catalog\Model\ResourceModel\Category', [], [], '', false); $this->category = $this->getMock( - 'Magento\Catalog\Model\Category', - ['setUrlKey', 'setUrlPath', 'dataHasChangedFor', 'isObjectNew', 'getResource', 'getUrlKey', 'getStoreId'], + \Magento\Catalog\Model\Category::class, + [ + 'setUrlKey', + 'setUrlPath', + 'dataHasChangedFor', + 'isObjectNew', + 'getResource', + 'getUrlKey', + 'getStoreId', + 'getData' + ], [], '', false @@ -55,18 +73,18 @@ protected function setUp() $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); $this->observer->expects($this->any())->method('getCategory')->willReturn($this->category); $this->categoryUrlPathGenerator = $this->getMock( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class, [], [], '', false ); $this->childrenCategoriesProvider = $this->getMock( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider::class ); $this->storeViewService = $this->getMock( - 'Magento\CatalogUrlRewrite\Service\V1\StoreViewService', + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class, [], [], '', @@ -74,7 +92,7 @@ protected function setUp() ); $this->categoryUrlPathAutogeneratorObserver = (new ObjectManagerHelper($this))->getObject( - 'Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver', + \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver::class, [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, @@ -90,7 +108,7 @@ public function testSetCategoryUrlAndCategoryPath() $this->category->expects($this->once())->method('setUrlKey')->with('urk_key')->willReturnSelf(); $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn('url_path'); $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } @@ -114,7 +132,7 @@ public function testUrlKeyAndUrlPathUpdating() $this->category->expects($this->once())->method('setUrlKey')->with('url_key')->willReturnSelf(); $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); // break code execution - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } @@ -128,7 +146,7 @@ public function testUrlPathAttributeNoUpdatingIfCategoryIsNew() $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryResource->expects($this->never())->method('saveAttribute'); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); @@ -142,7 +160,7 @@ public function testUrlPathAttributeUpdating() $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); @@ -162,7 +180,7 @@ public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChange $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); @@ -177,7 +195,7 @@ public function testChildrenUrlPathAttributeUpdatingForSpecificStore() $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->any())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->category->expects($this->any())->method('dataHasChangedFor')->willReturn(true); // only for specific store $this->category->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.php index d0d208d5faf7b..1dc96e39ca026 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.php @@ -53,9 +53,14 @@ public function fill(FixtureInterface $fixture, SimpleElement $element = null) $storeSwitcherBlock->find($this->dropdownBlock, Locator::SELECTOR_CSS, 'liselectstore')->setValue($store); $modalElement = $this->browser->find($this->confirmModal); /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ - $modal = $this->blockFactory->create('Magento\Ui\Test\Block\Adminhtml\Modal', ['element' => $modalElement]); + $modal = $this->blockFactory->create( + \Magento\Ui\Test\Block\Adminhtml\Modal::class, + ['element' => $modalElement] + ); $modal->acceptAlert(); + $modal->waitModalWindowToDisappear(); } + return parent::fill($fixture, $element); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml index 09240d9cf319b..6fa3b543f38c5 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/CategoryForm.xml @@ -24,7 +24,7 @@ input[name='name'] - [name="use_default[]"][value="name"] + [name="use_default[name]"] checkbox @@ -90,6 +90,10 @@ input input[name='url_key'] + + checkbox + input[name='use_default[url_key]'] + input input[name='meta_title'] diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/PageActions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/PageActions.php index 3bf28dd49164a..3adee2a605e96 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/PageActions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Edit/PageActions.php @@ -7,12 +7,18 @@ namespace Magento\Catalog\Test\Block\Adminhtml\Category\Edit; use Magento\Backend\Test\Block\FormPageActions; +use Magento\Mtf\Client\Locator; /** * Category page actions. */ class PageActions extends FormPageActions { + /** + * Top page element to implement a scrolling in case of floating blocks overlay. + */ + const TOP_ELEMENT_TO_SCROLL = 'header.page-header'; + /** * Locator for "OK" button in warning block * @@ -20,6 +26,20 @@ class PageActions extends FormPageActions */ protected $warningBlock = '.ui-widget-content .ui-dialog-buttonset button:first-child'; + /** + * Change Store View selector. + * + * @var string + */ + protected $storeChangeButton = '#store-change-button'; + + /** + * Selector for confirm. + * + * @var string + */ + protected $confirmModal = '.confirm._show[data-role=modal]'; + /** * Click on "Save" button * @@ -33,4 +53,23 @@ public function save() $warningBlock->click(); } } + + /** + * Select Store View. + * + * @param string $name + * @return void + */ + public function selectStoreView($name) + { + $this->browser->find(self::TOP_ELEMENT_TO_SCROLL)->hover(); + $this->_rootElement->find($this->storeChangeButton)->click(); + $this->waitForElementVisible($name, Locator::SELECTOR_LINK_TEXT); + $this->_rootElement->find($name, Locator::SELECTOR_LINK_TEXT)->click(); + $element = $this->browser->find($this->confirmModal); + /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ + $modal = $this->blockFactory->create(\Magento\Ui\Test\Block\Adminhtml\Modal::class, ['element' => $element]); + $modal->acceptAlert(); + $this->waitForElementVisible($this->storeChangeButton); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForm.php index 4db4a9b12696a..c7a9cdd94a060 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForm.php @@ -27,7 +27,8 @@ class AssertCategoryForm extends AbstractAssertForm */ protected $skippedFixtureFields = [ 'parent_id', - 'id' + 'id', + 'store_id', ]; /** @@ -45,7 +46,10 @@ public function processAssert( ) { $catalogCategoryIndex->open(); $catalogCategoryIndex->getTreeCategories()->selectCategory($category, true); - + if ($category->hasData('store_id')) { + $storeName = $category->getStoreId()['source']->getName(); + $catalogCategoryEdit->getFormPageActions()->selectStoreView($storeName); + } $fixtureData = $this->prepareFixtureData($category->getData()); $formData = $catalogCategoryEdit->getEditForm()->getData($category); $error = $this->verifyData($this->sortData($fixtureData), $this->sortData($formData)); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml index 61a8981b0f3af..e35932a2c5ae4 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml @@ -20,7 +20,7 @@ - + @@ -41,6 +41,7 @@ + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml index 88f2670684816..46a7b9b22cf21 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml @@ -17,6 +17,17 @@ + + Category%isolation% + custom%isolation% + Yes + Yes + + default_category + + Yes + + Default Category 1 diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.php index 24db0bf77a42f..e5bf7085fdb9b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.php @@ -61,14 +61,12 @@ class UpdateCategoryEntityTest extends Injectable /** * Inject page end prepare default category * - * @param Category $initialCategory * @param CatalogCategoryIndex $catalogCategoryIndex * @param CatalogCategoryEdit $catalogCategoryEdit * @param FixtureFactory $fixtureFactory - * @return array + * @return void */ public function __inject( - Category $initialCategory, CatalogCategoryIndex $catalogCategoryIndex, CatalogCategoryEdit $catalogCategoryEdit, FixtureFactory $fixtureFactory @@ -76,8 +74,6 @@ public function __inject( $this->fixtureFactory = $fixtureFactory; $this->catalogCategoryIndex = $catalogCategoryIndex; $this->catalogCategoryEdit = $catalogCategoryEdit; - $initialCategory->persist(); - return ['initialCategory' => $initialCategory]; } /** @@ -89,6 +85,7 @@ public function __inject( */ public function test(Category $category, Category $initialCategory) { + $initialCategory->persist(); $this->catalogCategoryIndex->open(); $this->catalogCategoryIndex->getTreeCategories()->selectCategory($initialCategory); $this->catalogCategoryEdit->getEditForm()->fill($category); @@ -110,11 +107,16 @@ protected function prepareCategory(Category $category, Category $initialCategory ? $category->getDataFieldConfig('parent_id')['source']->getParentCategory() : $initialCategory->getDataFieldConfig('parent_id')['source']->getParentCategory(); + $rewriteData = ['parent_id' => ['source' => $parentCategory]]; + if ($category->hasData('store_id')) { + $rewriteData['store_id'] = ['source' => $category->getDataFieldConfig('store_id')['source']->getStore()]; + } + $data = [ 'data' => array_merge( $initialCategory->getData(), $category->getData(), - ['parent_id' => ['source' => $parentCategory]] + $rewriteData ) ]; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml index 2ed97cfd61834..9060adae6f487 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml @@ -51,5 +51,14 @@ + + + default_with_custom_url + default_category + custom + Yes + + + From 17b7855e3520b5158b8b25d719e7372ef0ea1fc4 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 11 May 2017 21:46:42 +0300 Subject: [PATCH 071/363] MAGETWO-62025: Upgrading from 2.0.11 with Sample Data fails --- .../Magento/Catalog/Setup/UpgradeData.php | 163 +++++++++++------- 1 file changed, 102 insertions(+), 61 deletions(-) diff --git a/app/code/Magento/Catalog/Setup/UpgradeData.php b/app/code/Magento/Catalog/Setup/UpgradeData.php index 9961441cd5bda..b3a30d8fa21da 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeData.php +++ b/app/code/Magento/Catalog/Setup/UpgradeData.php @@ -5,13 +5,10 @@ */ namespace Magento\Catalog\Setup; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Eav\Setup\EavSetup; -use Magento\Eav\Setup\EavSetupFactory; /** * Upgrade Data script @@ -29,20 +26,32 @@ class UpgradeData implements UpgradeDataInterface /** * EAV setup factory * - * @var EavSetupFactory + * @var \Magento\Eav\Setup\EavSetupFactory */ private $eavSetupFactory; + /** + * Attributes cache management. + * + * @var \Magento\Eav\Model\Entity\AttributeCache + */ + private $attributeCache; + /** * Init * * @param CategorySetupFactory $categorySetupFactory - * @param EavSetupFactory $eavSetupFactory + * @param \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory + * @param \Magento\Eav\Model\Entity\AttributeCache $attributeCache */ - public function __construct(CategorySetupFactory $categorySetupFactory, EavSetupFactory $eavSetupFactory) - { + public function __construct( + CategorySetupFactory $categorySetupFactory, + \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory, + \Magento\Eav\Model\Entity\AttributeCache $attributeCache + ) { $this->categorySetupFactory = $categorySetupFactory; $this->eavSetupFactory = $eavSetupFactory; + $this->attributeCache = $attributeCache; } /** @@ -87,7 +96,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface if (version_compare($context->getVersion(), '2.0.2') < 0) { // set new resource model paths - /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + /** @var CategorySetup $categorySetup */ $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); $categorySetup->updateEntityType( \Magento\Catalog\Model\Category::ENTITY, @@ -128,69 +137,75 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface } if (version_compare($context->getVersion(), '2.0.3') < 0) { - /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + /** @var CategorySetup $categorySetup */ $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); $categorySetup->updateAttribute(3, 54, 'default_value', 1); } if (version_compare($context->getVersion(), '2.0.4') < 0) { - /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + $mediaBackendType = 'static'; + $mediaBackendModel = null; + /** @var CategorySetup $categorySetup */ $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); $categorySetup->updateAttribute( 'catalog_product', 'media_gallery', 'backend_type', - 'static' + $mediaBackendType ); $categorySetup->updateAttribute( 'catalog_product', 'media_gallery', - 'backend_model' + 'backend_model', + $mediaBackendModel ); + + $this->changeMediaGalleryAttributeInCache($mediaBackendType, $mediaBackendModel); } if (version_compare($context->getVersion(), '2.0.5', '<')) { - /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + /** @var CategorySetup $categorySetup */ $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); //Product Details tab $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'status', 'frontend_label', 'Enable Product', 5 ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'name', 'frontend_label', 'Product Name' ); + $attributeSetId = $categorySetup->getDefaultAttributeSetId(\Magento\Catalog\Model\Product::ENTITY); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Product Details', 'visibility', 80 ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Product Details', 'news_from_date', 90 ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Product Details', 'news_to_date', 100 ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Product Details', 'country_of_manufacture', 110 @@ -198,27 +213,27 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface //Content tab $categorySetup->addAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Content', 15 ); $categorySetup->updateAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Content', 'tab_group_code', 'basic' ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Content', 'description' ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Content', 'short_description', 100 @@ -226,39 +241,39 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface //Images tab $groupId = (int)$categorySetup->getAttributeGroupByCode( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'image-management', 'attribute_group_id' ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, $groupId, 'image', 1 ); $categorySetup->updateAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, $groupId, 'attribute_group_name', 'Images' ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'image', 'frontend_label', 'Base' ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'small_image', 'frontend_label', 'Small' ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'image', 'frontend_input_renderer', null @@ -266,13 +281,13 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface //Design tab $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'page_layout', 'frontend_label', 'Layout' ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'custom_layout_update', 'frontend_label', 'Layout Update XML', @@ -281,56 +296,56 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface //Schedule Design Update tab $categorySetup->addAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Schedule Design Update', 55 ); $categorySetup->updateAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Schedule Design Update', 'tab_group_code', 'advanced' ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Schedule Design Update', 'custom_design_from', 20 ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Schedule Design Update', 'custom_design_to', 30 ); $categorySetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'custom_design', 'frontend_label', 'New Theme', 40 ); $categorySetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'Default', + \Magento\Catalog\Model\Product::ENTITY, + $attributeSetId, 'Schedule Design Update', 'custom_design' ); $categorySetup->addAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'custom_layout', [ 'type' => 'varchar', 'label' => 'New Layout', 'input' => 'select', - 'source' => 'Magento\Catalog\Model\Product\Attribute\Source\Layout', + 'source' => \Magento\Catalog\Model\Product\Attribute\Source\Layout::class, 'required' => false, 'sort_order' => 50, - 'global' => ScopedAttributeInterface::SCOPE_STORE, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, 'group' => 'Schedule Design Update', 'is_used_in_grid' => true, 'is_visible_in_grid' => false, @@ -338,13 +353,13 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface ] ); } - + if (version_compare($context->getVersion(), '2.0.7') < 0) { /** @var EavSetup $eavSetup */ - $eavSetup= $this->eavSetupFactory->create(['setup' => $setup]); + $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, + \Magento\Catalog\Model\Product::ENTITY, 'meta_description', [ 'note' => 'Maximum 255 chars. Meta Description should optimally be between 150-160 characters' @@ -353,7 +368,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface } if (version_compare($context->getVersion(), '2.1.3') < 0) { - /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + /** @var CategorySetup $categorySetup */ $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); $this->changePriceAttributeDefaultScope($categorySetup); } @@ -362,7 +377,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface } /** - * @param \Magento\Catalog\Setup\CategorySetup $categorySetup + * @param CategorySetup $categorySetup * @return void */ private function changePriceAttributeDefaultScope($categorySetup) @@ -379,4 +394,30 @@ private function changePriceAttributeDefaultScope($categorySetup) } } + + /** + * Change media_gallery attribute metadata in cache. + * + * @param string $mediaBackendType + * @param string $mediaBackendModel + * @return void + */ + private function changeMediaGalleryAttributeInCache($mediaBackendType, $mediaBackendModel) + { + // need to do, because media_gallery has backend model in cache. + $catalogProductAttributes = $this->attributeCache->getAttributes( + \Magento\Catalog\Model\Product::ENTITY, + '0-0' + ); + + if (is_array($catalogProductAttributes)) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $catalogProductAttribute */ + foreach ($catalogProductAttributes as $catalogProductAttribute) { + if ($catalogProductAttribute->getAttributeCode() == 'media_gallery') { + $catalogProductAttribute->setBackendModel($mediaBackendModel); + $catalogProductAttribute->setBackendType($mediaBackendType); + } + } + } + } } From c83c7207421e678948efe3a3da5675b988d5e01e Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 12 May 2017 10:43:32 +0300 Subject: [PATCH 072/363] MAGETWO-64243: Magento\Ui\Test\TestCase\GridSortingTest.test with data set "CmsBlocksGridSorting" --- .../TestSuite/InjectableTests/MAGETWO-64243.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml deleted file mode 100644 index efb40f03783eb..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64243.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 30af24a80b4fbd5eded461529f4a9d582c5c63b4 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 May 2017 10:47:47 +0300 Subject: [PATCH 073/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Handler/CatalogProductSimple/Curl.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php index b66b4fec7a2b8..3f5a146771aae 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php @@ -414,20 +414,26 @@ protected function prepareCategory() */ protected function prepareWebsites() { - if (isset($this->fixture->getDataFieldConfig('website_ids')['source'])) { - $webSitesSource = $this->fixture->getDataFieldConfig('website_ids')['source']; + if (!empty($this->fields['product']['website_ids'])) { + if (isset($this->fixture->getDataFieldConfig('website_ids')['source'])) { + $webSitesSource = $this->fixture->getDataFieldConfig('website_ids')['source']; - foreach ($webSitesSource->getWebsites() as $key => $website) { - $this->fields['product']['website_ids'][$key] = $website->getWebsiteId(); - } + foreach ($webSitesSource->getWebsites() as $key => $website) { + $this->fields['product']['website_ids'][$key] = $website->getWebsiteId(); + } - } else { - foreach ($this->fields['product']['website_ids'] as $key => $website) { - $website = isset($this->mappingData['website_ids'][$website]) - ? $this->mappingData['website_ids'][$website] - : $website; - $this->fields['product']['website_ids'][$key] = $website; + } else { + foreach ($this->fields['product']['website_ids'] as $key => $website) { + $website = isset($this->mappingData['website_ids'][$website]) + ? $this->mappingData['website_ids'][$website] + : $website; + $this->fields['product']['website_ids'][$key] = $website; + } } + } else { + $website = \Magento\Mtf\ObjectManagerFactory::getObjectManager() + ->create(\Magento\Store\Test\Fixture\Website::class, ['dataset' => 'default']); + $this->fields['product']['website_ids'][] = $website->getWebsiteId(); } } @@ -596,4 +602,6 @@ protected function prepareFpt() unset($this->fields['product']['fpt']); } } + + } From fe6440972105f8221eff23d63eb67c6d7faaf206 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Tue, 7 Feb 2017 17:39:42 +0200 Subject: [PATCH 074/363] MAGETWO-63578: [Backport] - [GitHub #5547] Configurable product wrongly imported if sku is an integer - for 2.1 --- .../Model/Import/Product.php | 2 +- .../Model/ConfigurableTest.php | 15 ++ .../Import/Product/Type/ConfigurableTest.php | 49 ++++--- .../_files/import_configurable_12345.csv | 4 + .../_files/product_configurable_12345.php | 133 ++++++++++++++++++ .../product_configurable_12345_rollback.php | 35 +++++ 6 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_12345.csv create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345_rollback.php diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index cbcbbd66897b1..ac5fe77c2498f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1249,7 +1249,7 @@ protected function _saveProductAttributes(array $attributesData) $linkId = $this->_connection->fetchOne( $this->_connection->select() ->from($this->getResource()->getTable('catalog_product_entity')) - ->where('sku = ?', $sku) + ->where('sku = ?', (string)$sku) ->columns($this->getProductEntityLinkField()) ); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php index 615c40498c94f..ec28cab76b87a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php @@ -7,8 +7,14 @@ use Magento\CatalogImportExport\Model\AbstractProductExportImportTestCase; +/** + * Configurable product import test. + */ class ConfigurableTest extends AbstractProductExportImportTestCase { + /** + * @return array + */ public function exportImportDataProvider() { return [ @@ -21,6 +27,15 @@ public function exportImportDataProvider() ], ['_cache_instance_products', '_cache_instance_configurable_attributes'], ], + 'configurable-product-12345' => [ + [ + 'Magento/ConfigurableProduct/_files/product_configurable_12345.php' + ], + [ + '12345', + ], + ['_cache_instance_products', '_cache_instance_configurable_attributes'], + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php index 8792e95736a34..210369c8ac6af 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php @@ -6,21 +6,17 @@ namespace Magento\ConfigurableImportExport\Model\Import\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import; /** + * Product type configurable import test. + * * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigurableTest extends \PHPUnit_Framework_TestCase { - /** - * Configurable product test Name - */ - const TEST_PRODUCT_NAME = 'Configurable 1'; - /** * Configurable product test Type */ @@ -31,13 +27,6 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase */ protected $model; - /** - * Configurable product options SKU list - * - * @var array - */ - protected $optionSkuList = ['Configurable 1-Option 1', 'Configurable 1-Option 2']; - /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -59,13 +48,35 @@ protected function setUp() } /** + * @return array + */ + public function configurableImportDataProvider() + { + return [ + 'Configurable 1' => [ + __DIR__ . '/../../_files/import_configurable.csv', + 'Configurable 1', + ['Configurable 1-Option 1', 'Configurable 1-Option 2'], + ], + '12345' => [ + __DIR__ . '/../../_files/import_configurable_12345.csv', + '12345', + ['Configurable 1-Option 1', 'Configurable 1-Option 2'], + ], + ]; + } + + /** + * @param string $pathToFile Path to import file + * @param string $productName Name/sku of configurable product + * @param array $optionSkuList Name of variations for configurable product * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_attribute.php * @magentoAppArea adminhtml + * @dataProvider configurableImportDataProvider */ - public function testConfigurableImport() + public function testConfigurableImport($pathToFile, $productName, $optionSkuList) { // import data from CSV file - $pathToFile = __DIR__ . '/../../_files/import_configurable.csv'; $filesystem = $this->objectManager->create( \Magento\Framework\Filesystem::class ); @@ -92,23 +103,23 @@ public function testConfigurableImport() /** @var \Magento\Catalog\Model\ResourceModel\Product $resource */ $resource = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); - $productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME); + $productId = $resource->getIdBySku($productName); $this->assertTrue(is_numeric($productId)); /** @var \Magento\Catalog\Model\Product $product */ $product = $this->objectManager->get(ProductRepositoryInterface::class)->getById($productId); $this->assertFalse($product->isObjectNew()); - $this->assertEquals(self::TEST_PRODUCT_NAME, $product->getName()); + $this->assertEquals($productName, $product->getName()); $this->assertEquals(self::TEST_PRODUCT_TYPE, $product->getTypeId()); $optionCollection = $product->getTypeInstance()->getConfigurableOptions($product); foreach ($optionCollection as $option) { foreach ($option as $optionData) { - $this->assertContains($optionData['sku'], $this->optionSkuList); + $this->assertContains($optionData['sku'], $optionSkuList); } } - $optionIdList = $resource->getProductsIdsBySkus($this->optionSkuList); + $optionIdList = $resource->getProductsIdsBySkus($optionSkuList); foreach ($optionIdList as $optionId) { $this->assertArrayHasKey($optionId, $product->getExtensionAttributes()->getConfigurableProductLinks()); } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_12345.csv b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_12345.csv new file mode 100644 index 0000000000000..02cfb46e920ca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_12345.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,tax_class_name,visibility,price,url_key,display_product_options_in,map_price,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,configurable_variations,configurable_variation_labels,associated_skus +Configurable 1-Option 1,,Default,simple,Configurable 1-Option 1,,,,1,Taxable Goods,Not Visible Individually,10,configurable-1-option-1,Block after Info Column,,"attribute_with_option=Option Label,has_options=0,quantity_and_stock_status=In Stock,required_options=0,test_configurable=Option 1",99999,0,0,0,0,1,1,0,0,0,1,,1,1,0,0,1,0,0,0,1,,, +Configurable 1-Option 2,,Default,simple,Configurable 1-Option 2,,,,1,Taxable Goods,Not Visible Individually,10,configurable-1-option-2,Block after Info Column,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0,test_configurable=Option 2",99999,0,0,0,0,1,1,0,0,0,1,,1,1,0,0,1,0,0,0,1,,, +12345,,Default,configurable,12345,,,,1,Taxable Goods,"Catalog, Search",10,12345,Block after Info Column,,"has_options=1,quantity_and_stock_status=In Stock,required_options=0",0,0,0,0,0,1,1,0,0,0,1,,1,0,0,0,1,0,0,0,1,"sku=Configurable 1-Option 1,test_configurable=Option 1|sku=Configurable 1-Option 2,test_configurable=Option 2",test_configurable=test_configurable, diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php new file mode 100644 index 0000000000000..d2fd3bd6fccc2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php @@ -0,0 +1,133 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productToDelete = $productRepository->getById(11); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { + // Nothing to remove +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345_rollback.php new file mode 100644 index 0000000000000..c40bd7692bbdc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_30', 'simple_40', '12345'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From e1e34debb33c50d02b1f3bfe97a0b94bcd2f9445 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Fri, 12 May 2017 12:27:45 +0300 Subject: [PATCH 075/363] MAGETWO-63587: [Backport] - Use default URL Key - for 2.1 --- .../Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml index 9060adae6f487..773dcda5d045d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml @@ -52,7 +52,7 @@ - + default_with_custom_url default_category custom From f8dedf7b3a3f3d2f17ba8d4cec225e55adb82244 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 12 May 2017 13:02:59 +0300 Subject: [PATCH 076/363] MAGETWO-63514: [Backport] - Free Shipping promo still applies for UPS after removing free ship item - for 2.1 --- .../Model/Quote/Address/Total/Shipping.php | 1 + .../Checkout/Test/Block/Cart/Totals.php | 11 ++ .../Quote/Address/Total/ShippingTest.php | 3 +- .../_files/cart_rule_free_shipping.php | 101 +++++++++--------- .../rule_free_shipping_by_product_weight.php | 43 ++++---- 5 files changed, 82 insertions(+), 77 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 4b91466195eb9..bf44378b3a543 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -180,6 +180,7 @@ public function collect( } } } + return $this; } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index 7e1c3ac5ff599..dc3354c3f0a21 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -107,6 +107,7 @@ public function getGrandTotal() { $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); + return $this->escapeCurrency($grandTotal); } @@ -118,6 +119,7 @@ public function getGrandTotal() public function getGrandTotalIncludingTax() { $priceElement = $this->_rootElement->find($this->grandTotalInclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -129,6 +131,7 @@ public function getGrandTotalIncludingTax() public function getGrandTotalExcludingTax() { $priceElement = $this->_rootElement->find($this->grandTotalExclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -140,6 +143,7 @@ public function getGrandTotalExcludingTax() public function getTax() { $priceElement = $this->_rootElement->find($this->tax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -161,6 +165,7 @@ public function isTaxVisible() public function getSubtotal() { $subTotal = $this->_rootElement->find($this->subtotal, Locator::SELECTOR_CSS)->getText(); + return $this->escapeCurrency($subTotal); } @@ -172,6 +177,7 @@ public function getSubtotal() public function getSubtotalIncludingTax() { $priceElement = $this->_rootElement->find($this->subtotalInclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -183,6 +189,7 @@ public function getSubtotalIncludingTax() public function getSubtotalExcludingTax() { $priceElement = $this->_rootElement->find($this->subtotalExclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -195,6 +202,7 @@ public function getSubtotalExcludingTax() protected function escapeCurrency($price) { preg_match("/^\\D*\\s*([\\d,\\.]+)\\s*\\D*$/", $price, $matches); + return (isset($matches[1])) ? $matches[1] : null; } @@ -206,6 +214,7 @@ protected function escapeCurrency($price) public function getDiscount() { $priceElement = $this->_rootElement->find($this->discount, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -217,6 +226,7 @@ public function getDiscount() public function getShippingPrice() { $priceElement = $this->_rootElement->find($this->shippingPriceSelector, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -228,6 +238,7 @@ public function getShippingPrice() public function getShippingPriceInclTax() { $priceElement = $this->_rootElement->find($this->shippingPriceInclTaxSelector, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php index 8e8466ab13717..dde854febc80d 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php @@ -47,7 +47,6 @@ public function testRuleByProductWeightWithFreeShipping() $this->assertTrue(count($methods) > 0); $this->assertEquals('flatrate', $methods[0]->getMethodCode()); $this->assertEquals(0, $methods[0]->getAmount()); - } /** @@ -64,7 +63,6 @@ public function testRuleByProductWeightWithoutFreeShipping() $this->assertTrue(count($methods) > 0); $this->assertEquals('flatrate', $methods[0]->getMethodCode()); $this->assertEquals(25, $methods[0]->getAmount()); - } /** @@ -83,6 +81,7 @@ private function estimateShipping($cartId) /** @var \Magento\Quote\Api\GuestShipmentEstimationInterface $estimation */ $estimation = $this->objectManager->get(\Magento\Quote\Api\GuestShipmentEstimationInterface::class); + return $estimation->estimateByExtendedAddress($cartId, $address); } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php index dc0b6e6f834c3..a2977132ad880 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php @@ -9,62 +9,59 @@ $salesRuleFactory = $objectManager->create(\Magento\SalesRule\Model\RuleFactory::class); /** @var \Magento\SalesRule\Model\Rule $salesRule */ $salesRule = $salesRuleFactory->create(); -$row = - [ - 'name' => 'Free shipping if item price >10', - 'is_active' => 1, - 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], - 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, - 'conditions' => [ - 1 => +$row = [ + 'name' => 'Free shipping if item price >10', + 'is_active' => 1, + 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [ + 1 => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + ] + ], + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, - 'attribute' => null, - 'operator' => null, - 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - ] - - ], - 'actions' => [ - 1 => [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, - 'attribute' => null, - 'operator' => null, - 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - 'conditions' => [ - [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, - 'attribute' => 'quote_item_price', - 'operator' => '==', - 'value' => '7', - 'is_value_processed' => false, - ] + 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'quote_item_price', + 'operator' => '==', + 'value' => '7', + 'is_value_processed' => false, ] ] - ], - 'is_advanced' => 1, - 'simple_action' => 'by_percent', - 'discount_amount' => 0, - 'stop_rules_processing' => 0, - 'discount_qty' => 0, - 'discount_step' => 0, - 'apply_to_shipping' => 1, - 'times_used' => 0, - 'is_rss' => 1, - 'use_auto_generation' => 0, - 'uses_per_coupon' => 0, - 'simple_free_shipping' => 1, - - 'website_ids' => [ - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite()->getId() ] - ]; + ], + 'is_advanced' => 1, + 'simple_action' => 'by_percent', + 'discount_amount' => 0, + 'stop_rules_processing' => 0, + 'discount_qty' => 0, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'times_used' => 0, + 'is_rss' => 1, + 'use_auto_generation' => 0, + 'uses_per_coupon' => 0, + 'simple_free_shipping' => 1, + + 'website_ids' => [ + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getId() + ] +]; $salesRule->loadPost($row); $salesRule->save(); /** @var Magento\Framework\Registry $registry */ diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php index 8dfef696b4fec..c02cc33423cd3 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php @@ -6,31 +6,28 @@ require 'cart_rule_free_shipping.php'; -$row = - [ - 'name' => 'Free shipping if item weight <= 1', - 'conditions' => [ - 1 => +$row = [ + 'name' => 'Free shipping if item weight <= 1', + 'conditions' => [ + 1 => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, - 'attribute' => null, - 'operator' => null, + 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + 'operator' => '<=', 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - 'conditions' => [ - [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, - 'attribute' => 'weight', - 'operator' => '<=', - 'value' => '1', - 'is_value_processed' => false, - ] - ] + 'is_value_processed' => false, ] - - ], - 'actions' => [], - ]; + ] + ] + ], + 'actions' => [], +]; $salesRule->loadPost($row); $salesRule->save(); From c71a789af177a9c5874fca166ef36d95b6aabbf6 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 May 2017 13:23:47 +0300 Subject: [PATCH 077/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Price/MinimalPriceCalculatorInterface.php | 6 ++--- .../Price/MinimalTierPriceCalculator.php | 16 ++++++++---- .../Catalog/Pricing/Render/FinalPriceBox.php | 26 ++++++++++++------- .../Test/Fixture/ConfigData/Section.php | 1 + 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php index aeaf31a0c8a22..de31b1d5b51a9 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php @@ -10,12 +10,12 @@ use Magento\Framework\Pricing\Amount\AmountInterface; /** - * Interface define methods which control display of "As low as" price + * Interface define methods which control display of "As low as" price. */ interface MinimalPriceCalculatorInterface { /** - * Get raw value for "as low as" price + * Get raw value for "as low as" price. * * @param SaleableInterface $saleableItem * @return float|null @@ -23,7 +23,7 @@ interface MinimalPriceCalculatorInterface public function getValue(SaleableInterface $saleableItem); /** - * Return structured object with "as low as" value + * Return structured object with "as low as" value. * * @param SaleableInterface $saleableItem * @return AmountInterface|null diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index e006d02126118..6ff81debab7c5 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -11,11 +11,13 @@ use Magento\Framework\Pricing\Amount\AmountInterface; /** - * MinimalTierPriceCalculator shows minimal value of Tier Prices + * MinimalTierPriceCalculator shows minimal value of Tier Prices. */ class MinimalTierPriceCalculator implements MinimalPriceCalculatorInterface { /** + * Price Calculator interface. + * * @var CalculatorInterface */ private $calculator; @@ -29,8 +31,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices. + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +53,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value. + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index 8741dcf648aac..afe3e0f7374a7 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -16,7 +16,7 @@ use Magento\Framework\View\Element\Template\Context; /** - * Class for final_price rendering + * Class for final_price rendering. * * @method bool getUseLinkForAsLowAs() * @method bool getDisplayMinimalPrice() @@ -24,14 +24,22 @@ class FinalPriceBox extends BasePriceBox { /** + * Interface resolver provided to check is product available for sale. + * * @var \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface */ private $salableResolver; - /** @var \Magento\Framework\Module\Manager */ + /** + * Module statuses manager. + * + * @var \Magento\Framework\Module\Manager + */ private $moduleManager; /** + * Shows minimal value of Tier Prices. + * * @var \Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface */ private $minimalPriceCalculator; @@ -117,7 +125,7 @@ private function isMsrpPriceApplicable() } /** - * Wrap with standard required container + * Wrap with standard required container. * * @param string $html * @return string @@ -131,7 +139,7 @@ protected function wrapResult($html) } /** - * Render minimal amount + * Render minimal amount. * * @return string */ @@ -156,7 +164,7 @@ public function renderAmountMinimal() } /** - * Define if the special price should be shown + * Define if the special price should be shown. * * @return bool */ @@ -168,7 +176,7 @@ public function hasSpecialPrice() } /** - * Define if the minimal price should be shown + * Define if the minimal price should be shown. * * @return bool */ @@ -186,7 +194,7 @@ public function showMinimalPrice() } /** - * Get Key for caching block content + * Get Key for caching block content. * * @return string */ @@ -221,8 +229,8 @@ private function getModuleManager() } /** - * Get flag that price rendering should be done for the list of products - * By default (if flag is not set) is false + * Get flag that price rendering should be done for the list of products. + * By default (if flag is not set) is false. * * @return bool */ diff --git a/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php index d8be9fe5baf0b..b25a7c383fa52 100644 --- a/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php +++ b/dev/tests/functional/tests/app/Magento/Config/Test/Fixture/ConfigData/Section.php @@ -129,6 +129,7 @@ private function replacePlaceholders(array $data) return $value; }, $params); } + return $data; } From b60b7495f9a941d6b68991a78535080c886bfb0b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 May 2017 13:47:49 +0300 Subject: [PATCH 078/363] MAGETWO-61729: "As low as" price shown if price different on global and store view level --- .../Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php index 3f5a146771aae..2440f6e800a18 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php @@ -602,6 +602,4 @@ protected function prepareFpt() unset($this->fields['product']['fpt']); } } - - } From 35059e45fa36e81112f22ca693265d972e1550cc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 May 2017 15:38:02 +0300 Subject: [PATCH 079/363] MAGETWO-63656: [Backport] - category_ids attribute scope dropdown - for 2.1 --- app/code/Magento/Catalog/etc/eav_attributes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Catalog/etc/eav_attributes.xml b/app/code/Magento/Catalog/etc/eav_attributes.xml index 133849a28e048..005402937232f 100644 --- a/app/code/Magento/Catalog/etc/eav_attributes.xml +++ b/app/code/Magento/Catalog/etc/eav_attributes.xml @@ -30,6 +30,7 @@ + From 7c6f1cd79da21899f624d3b7d7d0fe20262e6564 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 15 May 2017 10:38:21 +0300 Subject: [PATCH 080/363] MAGETWO-64297: No websites shown in grid --- .../Constraint/AssertDiscountInShoppingCart.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertDiscountInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertDiscountInShoppingCart.php index 468ecc2395ef6..c063e27836161 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertDiscountInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertDiscountInShoppingCart.php @@ -8,6 +8,7 @@ use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; +use Magento\Customer\Test\Fixture\Customer; use Magento\Mtf\Constraint\AbstractConstraint; /** @@ -20,12 +21,21 @@ class AssertDiscountInShoppingCart extends AbstractConstraint /** * Assert that discount is equal to expected. * + * @param Customer $customer * @param CheckoutCart $checkoutCart * @param Cart $cart * @return void */ - public function processAssert(CheckoutCart $checkoutCart, Cart $cart) - { + public function processAssert( + Customer $customer, + CheckoutCart $checkoutCart, + Cart $cart + ) { + $loginStep = $this->objectManager->create( + \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep::class, + ['customer' => $customer] + ); + $loginStep->run(); $checkoutCart->open(); $checkoutCart->getTotalsBlock()->waitForUpdatedTotals(); \PHPUnit_Framework_Assert::assertEquals( @@ -33,6 +43,7 @@ public function processAssert(CheckoutCart $checkoutCart, Cart $cart) $checkoutCart->getTotalsBlock()->getDiscount(), 'Discount amount in the shopping cart not equals to discount amount from fixture.' ); + $loginStep->cleanUp(); } /** From a0492ba0a3a3d45a95a26e60a568e31c39995012 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 15 May 2017 13:29:21 +0300 Subject: [PATCH 081/363] MAGETWO-64297: No websites shown in grid --- .../TestSuite/InjectableTests/MAGETWO-64297.xml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml deleted file mode 100644 index 0a292b70c0f19..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64297.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file From 6cb15936476a83743d315a64c1ab008216d0d3f8 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Mon, 15 May 2017 17:18:05 +0300 Subject: [PATCH 082/363] MAGETWO-61907: [Backport] - Updating customer via REST API without address unsets default billing and default shipping address - for 2.1 --- .../Magento/Customer/Model/ResourceModel/CustomerRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 386e1259f86c5..d037f866ffaca 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -323,7 +323,7 @@ protected function addFilterGroupToCollection( * @return void * @throws \Magento\Framework\Exception\InputException */ - private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInterface $customer, int $customerId) + private function updateAddresses(\Magento\Framework\Api\CustomAttributesDataInterface $customer, $customerId) { if ($customer->getAddresses() !== null) { if ($customer->getId()) { From 7a3c190c07d0264618d2bbc9528d37c54149b6bd Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 17 May 2017 14:42:46 +0300 Subject: [PATCH 083/363] MAGETWO-63814: [Backport] - Auto Generated coupon codes not applying - for 2.1 --- .../Model/ResourceModel/Rule/Collection.php | 12 +++++++-- .../Model/Service/CouponManagementService.php | 2 +- .../AssertCartPriceRuleApplying.php | 21 +++++++++++---- .../TestCase/CreateSalesRuleEntityTest.php | 13 +++++---- .../TestCase/CreateSalesRuleEntityTest.xml | 27 +++++++++++++++++++ .../Magento/SalesRule/_files/coupons.php | 16 +++++------ 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 35b20dccf2988..c75113ba1342c 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -156,11 +156,19 @@ public function setValidationFilter( \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON ); - $orWhereConditions = [ + $autoGeneratedCouponCondition = [ $connection->quoteInto( - '(main_table.coupon_type = ? AND rule_coupons.type = 0)', + "main_table.coupon_type = ?", \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO ), + $connection->quoteInto( + "rule_coupons.type = ?", + \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED + ), + ]; + + $orWhereConditions = [ + "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", $connection->quoteInto( '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC diff --git a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php index a69e17478dea4..dfad27005e7b4 100644 --- a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php +++ b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php @@ -88,7 +88,7 @@ public function generate(\Magento\SalesRule\Api\Data\CouponGenerationSpecInterfa $couponSpec->getRuleId() ); } - if (!$rule->getUseAutoGeneration()) { + if (!($rule->getUseAutoGeneration() || $rule->getCouponType() == $rule::COUPON_TYPE_AUTO)) { throw new \Magento\Framework\Exception\LocalizedException( __('Specified rule does not allow automatic coupon generation') ); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php index 54de53626dfba..79cdd6efefa15 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php @@ -137,6 +137,7 @@ abstract protected function assert(); * @param int|null $isLoggedIn * @param array $shipping * @param array $cartPrice + * @param array $couponCode * @return void * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -157,7 +158,8 @@ public function processAssert( Address $address = null, $isLoggedIn = null, array $shipping = [], - array $cartPrice = [] + array $cartPrice = [], + $couponCodes = null ) { $this->checkoutCart = $checkoutCart; $this->cmsIndex = $cmsIndex; @@ -179,10 +181,19 @@ public function processAssert( $this->checkoutCart->getShippingBlock()->fillEstimateShippingAndTax($address); $this->checkoutCart->getShippingBlock()->selectShippingMethod($shipping); } - if ($salesRule->getCouponCode() || $salesRuleOrigin->getCouponCode()) { - $this->checkoutCart->getDiscountCodesBlock()->applyCouponCode( - $salesRule->getCouponCode() ? $salesRule->getCouponCode() : $salesRuleOrigin->getCouponCode() - ); + + $couponCode = null; + + if ($salesRule->getCouponCode()) { + $couponCode = $salesRule->getCouponCode(); + } elseif ($salesRuleOrigin->getCouponCode()) { + $couponCode = $salesRuleOrigin->getCouponCode(); + } elseif ($couponCodes && is_array($couponCodes)) { + $couponCode = isset($couponCodes[0]) ? $couponCodes[0]: null; + } + + if ($salesRule->getCouponCode() || $salesRuleOrigin->getCouponCode() || $couponCode) { + $this->checkoutCart->getDiscountCodesBlock()->applyCouponCode($couponCode); } $this->assert(); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index 930ac94ffa37f..b9899fd2de2da 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -102,10 +102,10 @@ public function __inject( * * @param SalesRule $salesRule * @param CatalogProductSimple $productForSalesRule1 - * @param CatalogProductSimple $productForSalesRule2 - * @param Customer $customer - * @param string $conditionEntity - * @param array $generateCouponsSettings + * @param CatalogProductSimple|null $productForSalesRule2 + * @param Customer|null $customer + * @param null $conditionEntity + * @param array|null $generateCouponsSettings * * @return array */ @@ -137,7 +137,10 @@ public function testCreateSalesRule( $this->promoQuoteNew->open(); $this->promoQuoteNew->getSalesRuleForm()->fill($salesRule, null, $replace); - if ($salesRule->getUseAutoGeneration() == 'Yes' && !empty($generateCouponsSettings)) { + if ( + ($salesRule->getUseAutoGeneration() == 'Yes' || $salesRule->getCouponType() == "Auto") + && !empty($generateCouponsSettings) + ) { $this->promoQuoteNew->getFormPageActions()->saveAndContinue(); /** @var PromoQuoteForm $salesRuleForm */ diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 430a31731dab5..6327eb7ed2fdf 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -426,5 +426,32 @@ + + United States + California + 95814 + Flat Rate + Fixed + Cart Price Rule2 %isolation% + Cart Price Rule Description %isolation% + Yes + Main Website + NOT LOGGED IN + Auto + Fixed amount discount + 35 + No + No + Coupon code+fixed amount discount + 1 + simple_for_salesrule_1 + 2 + 200.00 + 140.00 + 70.00 + + + + diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php index ab9bc20f2bfaf..e8b07616b637b 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php @@ -5,26 +5,26 @@ */ $this->_collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\SalesRule\Model\ResourceModel\Rule\Collection' + \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); $items = array_values($this->_collection->getItems()); // type SPECIFIC with code -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); $coupon->setRuleId($items[0]->getId())->setCode('coupon_code')->setType(0)->save(); // type NO_COUPON with non actual previously generated coupon codes -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); $coupon->setRuleId($items[1]->getId())->setCode('autogenerated_2_1')->setType(1)->save(); -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); $coupon->setRuleId($items[1]->getId())->setCode('autogenerated_2_2')->setType(1)->save(); // type SPECIFIC with generated coupons -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); $coupon->setRuleId($items[2]->getId())->setCode('autogenerated_3_1')->setType(1)->save(); -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); $coupon->setRuleId($items[2]->getId())->setCode('autogenerated_3_2')->setType(1)->save(); // type AUTO -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\SalesRule\Model\Coupon'); -$coupon->setRuleId($items[3]->getId())->setCode('coupon_code_auto')->setType(0)->save(); +$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); +$coupon->setRuleId($items[3]->getId())->setCode('coupon_code_auto')->setType(1)->save(); From 22b92abd0fce81d3cd3c4480c2034ae39b3b43fc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 17 May 2017 15:32:42 +0300 Subject: [PATCH 084/363] MAGETWO-63814: [Backport] - Auto Generated coupon codes not applying - for 2.1 --- .../TestCase/CreateSalesRuleEntityTest.xml | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 6327eb7ed2fdf..430a31731dab5 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -426,32 +426,5 @@ - - United States - California - 95814 - Flat Rate - Fixed - Cart Price Rule2 %isolation% - Cart Price Rule Description %isolation% - Yes - Main Website - NOT LOGGED IN - Auto - Fixed amount discount - 35 - No - No - Coupon code+fixed amount discount - 1 - simple_for_salesrule_1 - 2 - 200.00 - 140.00 - 70.00 - - - - From 4437b393ae17cf4ebba11db390ac391a0c67fd70 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 17 May 2017 16:46:28 +0300 Subject: [PATCH 085/363] MAGETWO-63814: [Backport] - Auto Generated coupon codes not applying - for 2.1 --- .../SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php index 79cdd6efefa15..da9098ef6d8d9 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php @@ -141,6 +141,8 @@ abstract protected function assert(); * @return void * * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function processAssert( CheckoutCart $checkoutCart, From 3a4fb597e380a66b561059b3d5f21509f46a9d30 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 18 May 2017 12:29:58 +0300 Subject: [PATCH 086/363] MAGETWO-63819: [Backport] - Order grid displays wrong Purchase Date for order - for 2.1 --- .../Sales/view/adminhtml/ui_component/sales_order_grid.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index ec0ba345bc216..cf202e51578bc 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -186,7 +186,7 @@ Magento_Ui/js/grid/columns/date date Purchase Date - MMM dd, YYYY, H:MM:SS A + MMM dd, YYYY, H:mm:ss A From 884f09d87b0a9d4bdc9951dbb0a6847d86a593fc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 18 May 2017 14:09:10 +0300 Subject: [PATCH 087/363] MAGETWO-62628: [Backport] Fix functionality for functional tests --- .../view/frontend/web/js/model/gift-message.js | 2 +- .../tests/app/Magento/Checkout/Test/Block/Cart.php | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js b/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js index bd9da994f9639..c64c4c0012381 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js +++ b/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js @@ -78,7 +78,7 @@ define(['uiElement', 'underscore', 'mage/url'], }, afterSubmit: function() { window.location.href = url.build('checkout/cart/updatePost') - + '?form_key=' + window.giftOptionsConfig.giftMessage.formKey + + '?form_key=' + window.checkoutConfig.formKey + '&cart[]'; }, getSubmitParams: function(remove) { diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php index afa9f4c460074..4936bb7922716 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php @@ -99,6 +99,13 @@ class Cart extends Block */ protected $preloaderSpinner = '#preloaderSpinner'; + /** + * Cart item class name. + * + * @var string + */ + protected $cartItemClass = \Magento\Checkout\Test\Block\Cart\CartItem::class; + /** * Wait for PayPal page is loaded. * @@ -129,7 +136,7 @@ public function getCartItem(FixtureInterface $product) Locator::SELECTOR_XPATH ); $cartItem = $this->blockFactory->create( - 'Magento\Checkout\Test\Block\Cart\CartItem', + $this->cartItemClass, ['element' => $cartItemBlock] ); } From cc80f72bd667b76c1da6118932bd853261f444ec Mon Sep 17 00:00:00 2001 From: Bohdan Korablov Date: Wed, 5 Oct 2016 12:11:48 +0300 Subject: [PATCH 088/363] MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit fc99447f6aa1abfc1003f4181cea916c1616ea9e) MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit a9697c672e371f4484bb6ab0307d59bc346e8625) MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit 7352b7cc7616b4b0de0ee3109983ec64abb7ccd8) MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit e8712764a918c0814c789b57d892963ac63305c5) MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit 01f1f42b2431c8ce0b23c260a1af0261c02cb81c) MAGETWO-59256: [GitHub] Custom composer modules break Component Manager #6718 (cherry picked from commit e072d65045ce8a3063ce07a164a06f7d3c09b0ac) --- .../src/Magento/Setup/Model/PackagesData.php | 78 ++++-- .../Test/Unit/Model/PackagesDataTest.php | 258 +++++++++++++++--- 2 files changed, 274 insertions(+), 62 deletions(-) diff --git a/setup/src/Magento/Setup/Model/PackagesData.php b/setup/src/Magento/Setup/Model/PackagesData.php index f98ada3614687..055cb53e6af71 100644 --- a/setup/src/Magento/Setup/Model/PackagesData.php +++ b/setup/src/Magento/Setup/Model/PackagesData.php @@ -326,6 +326,32 @@ public function getPackagesForInstall() } /** + * Filter packages by allowed types + * + * @param array $packages + * @return array + */ + private function filterPackagesList(array $packages) + { + return array_filter( + $packages, + function ($item) { + return in_array( + $item['package_type'], + [ + \Magento\Setup\Model\Grid\TypeMapper::LANGUAGE_PACKAGE_TYPE, + \Magento\Setup\Model\Grid\TypeMapper::MODULE_PACKAGE_TYPE, + \Magento\Setup\Model\Grid\TypeMapper::EXTENSION_PACKAGE_TYPE, + \Magento\Setup\Model\Grid\TypeMapper::THEME_PACKAGE_TYPE, + \Magento\Setup\Model\Grid\TypeMapper::METAPACKAGE_PACKAGE_TYPE + ] + ); + } + ); + } + + /** + * Get MetaPackage for package * * @param array $packages * @return array @@ -377,26 +403,38 @@ private function getPackageAvailableVersions($package) return array_keys($packageVersions); } - } else { - $versionsPattern = '/^versions\s*\:\s(.+)$/m'; - - $commandParams = [ - self::PARAM_COMMAND => self::COMPOSER_SHOW, - self::PARAM_PACKAGE => $package, - self::PARAM_AVAILABLE => true - ]; - - $applicationFactory = $this->objectManagerProvider->get() - ->get('Magento\Framework\Composer\MagentoComposerApplicationFactory'); - /** @var \Magento\Composer\MagentoComposerApplication $application */ - $application = $applicationFactory->create(); - - $result = $application->runComposerCommand($commandParams); - $matches = []; - preg_match($versionsPattern, $result, $matches); - if (isset($matches[1])) { - return explode(', ', $matches[1]); - } + } + + return $this->getAvailableVersionsFromAllRepositories($package); + } + + /** + * Get available versions of package by "composer show" command + * + * @param string $package + * @return array + * @exception \RuntimeException + */ + private function getAvailableVersionsFromAllRepositories($package) + { + $versionsPattern = '/^versions\s*\:\s(.+)$/m'; + + $commandParams = [ + self::PARAM_COMMAND => self::COMPOSER_SHOW, + self::PARAM_PACKAGE => $package, + self::PARAM_AVAILABLE => true + ]; + + $applicationFactory = $this->objectManagerProvider->get() + ->get(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class); + /** @var \Magento\Composer\MagentoComposerApplication $application */ + $application = $applicationFactory->create(); + + $result = $application->runComposerCommand($commandParams); + $matches = []; + preg_match($versionsPattern, $result, $matches); + if (isset($matches[1])) { + return explode(', ', $matches[1]); } throw new \RuntimeException( diff --git a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php index 96509f4ece2bd..ee1774bc187b7 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php @@ -18,47 +18,96 @@ class PackagesDataTest extends \PHPUnit_Framework_TestCase */ private $packagesData; + /** + * @var ComposerInformation|MockObject + */ + private $composerInformation; + + /** + * @var \Magento\Setup\Model\DateTime\TimeZoneProvider|MockObject + */ + private $timeZoneProvider; + + /** + * @var \Magento\Setup\Model\PackagesAuth|MockObject + */ + private $packagesAuth; + + /** + * @var \Magento\Framework\Filesystem|MockObject + */ + private $filesystem; + + /** + * @var \Magento\Setup\Model\ObjectManagerProvider|MockObject + */ + private $objectManagerProvider; + + /** + * @var \Magento\Setup\Model\Grid\TypeMapper|MockObject + */ + private $typeMapper; + public function setUp() { - $composerInformation = $this->getMock('\Magento\Framework\Composer\ComposerInformation', [], [], '', false); - $composerInformation->expects($this->any())->method('getInstalledMagentoPackages')->willReturn( - [ - ['name' => 'magento/package-1', 'type' => 'magento2-module', 'version'=> '1.0.0'], - ['name' => 'magento/package-2', 'type' => 'magento2-module', 'version'=> '1.0.1'] - ] - ); - - $composerInformation->expects($this->any())->method('getRootRepositories')->willReturn(['repo1', 'repo2']); - $composerInformation->expects($this->any())->method('getPackagesTypes')->willReturn(['magento2-module']); - $timeZoneProvider = $this->getMock('\Magento\Setup\Model\DateTime\TimeZoneProvider', [], [], '', false); - $timeZone = $this->getMock('\Magento\Framework\Stdlib\DateTime\Timezone', [], [], '', false); - $timeZoneProvider->expects($this->any())->method('get')->willReturn($timeZone); - $packagesAuth = $this->getMock('\Magento\Setup\Model\PackagesAuth', [], [], '', false); - $filesystem = $this->getMock('\Magento\Framework\Filesystem', [], [], '', false); - $objectManagerProvider = $this->getMock('\Magento\Setup\Model\ObjectManagerProvider', [], [], '', false); - $objectManager = $this->getMockForAbstractClass('\Magento\Framework\ObjectManagerInterface'); - $applicationFactory = $this->getMock( - '\Magento\Framework\Composer\MagentoComposerApplicationFactory', - [], - [], - '', - false - ); - $application = $this->getMock('\Magento\Composer\MagentoComposerApplication', [], [], '', false); + $this->composerInformation = $this->getComposerInformation(); + $this->timeZoneProvider = $this->getMockBuilder(\Magento\Setup\Model\DateTime\TimeZoneProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $timeZone = $this->getMock(\Magento\Framework\Stdlib\DateTime\Timezone::class, [], [], '', false); + $this->timeZoneProvider->expects($this->any())->method('get')->willReturn($timeZone); + $this->packagesAuth = $this->getMock(\Magento\Setup\Model\PackagesAuth::class, [], [], '', false); + $this->filesystem = $this->getMock(\Magento\Framework\Filesystem::class, [], [], '', false); + $this->objectManagerProvider = $this->getMockBuilder(\Magento\Setup\Model\ObjectManagerProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = $this->getMockForAbstractClass(\Magento\Framework\ObjectManagerInterface::class); + $appFactory = $this->getMockBuilder(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $application = $this->getMock(\Magento\Composer\MagentoComposerApplication::class, [], [], '', false); $application->expects($this->any()) ->method('runComposerCommand') - ->willReturn('versions: 2.0.1'); - $applicationFactory->expects($this->any())->method('create')->willReturn($application); + ->willReturnMap([ + [ + [ + PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, + PackagesData::PARAM_PACKAGE => 'magento/package-1', + PackagesData::PARAM_AVAILABLE => true, + ], + null, + 'versions: 2.0.1' + ], + [ + [ + PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, + PackagesData::PARAM_PACKAGE => 'magento/package-2', + PackagesData::PARAM_AVAILABLE => true, + ], + null, + 'versions: 2.0.1' + ], + [ + [ + PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, + PackagesData::PARAM_PACKAGE => 'partner/package-3', + PackagesData::PARAM_AVAILABLE => true, + ], + null, + 'versions: 3.0.1' + ], + ]); + $appFactory->expects($this->any())->method('create')->willReturn($application); $objectManager->expects($this->any()) ->method('get') - ->with('Magento\Framework\Composer\MagentoComposerApplicationFactory') - ->willReturn($applicationFactory); - $objectManagerProvider->expects($this->any())->method('get')->willReturn($objectManager); - - $directoryWrite = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\WriteInterface'); - $directoryRead = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\ReadInterface'); - $filesystem->expects($this->any())->method('getDirectoryRead')->will($this->returnValue($directoryRead)); - $filesystem->expects($this->any()) + ->with(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class) + ->willReturn($appFactory); + $this->objectManagerProvider->expects($this->any())->method('get')->willReturn($objectManager); + + $directoryWrite = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); + $directoryRead = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class); + $this->filesystem->expects($this->any())->method('getDirectoryRead')->will($this->returnValue($directoryRead)); + $this->filesystem->expects($this->any()) ->method('getDirectoryWrite') ->will($this->returnValue($directoryWrite)); $directoryWrite->expects($this->any())->method('isExist')->willReturn(true); @@ -84,13 +133,80 @@ public function setUp() . '}}}' ); + $this->typeMapper = $this->getMockBuilder(\Magento\Setup\Model\Grid\TypeMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->typeMapper->expects(static::any()) + ->method('map') + ->willReturnMap([ + [ComposerInformation::MODULE_PACKAGE_TYPE, \Magento\Setup\Model\Grid\TypeMapper::MODULE_PACKAGE_TYPE], + ]); + + $this->createPackagesData(); + } + + private function createPackagesData() + { $this->packagesData = new PackagesData( - $composerInformation, - $timeZoneProvider, - $packagesAuth, - $filesystem, - $objectManagerProvider + $this->composerInformation, + $this->timeZoneProvider, + $this->packagesAuth, + $this->filesystem, + $this->objectManagerProvider, + $this->typeMapper + ); + } + + /** + * @param array $requiredPackages + * @param array $installedPackages + * @param array $repo + * @return ComposerInformation|MockObject + */ + private function getComposerInformation($requiredPackages = [], $installedPackages = [], $repo = []) + { + $composerInformation = $this->getMock(ComposerInformation::class, [], [], '', false); + $composerInformation->expects($this->any())->method('getInstalledMagentoPackages')->willReturn( + $installedPackages ?: + [ + 'magento/package-1' => [ + 'name' => 'magento/package-1', + 'type' => 'magento2-module', + 'version'=> '1.0.0' + ], + 'magento/package-2' => [ + 'name' => 'magento/package-2', + 'type' => 'magento2-module', + 'version'=> '1.0.1' + ], + 'partner/package-3' => [ + 'name' => 'partner/package-3', + 'type' => 'magento2-module', + 'version'=> '3.0.0' + ], + ] ); + + $composerInformation->expects($this->any())->method('getRootRepositories') + ->willReturn($repo ?: ['repo1', 'repo2']); + $composerInformation->expects($this->any())->method('getPackagesTypes') + ->willReturn(['magento2-module']); + $rootPackage = $this->getMock(RootPackage::class, [], ['magento/project', '2.1.0', '2']); + $rootPackage->expects($this->any()) + ->method('getRequires') + ->willReturn( + $requiredPackages ?: + [ + 'magento/package-1' => '1.0.0', + 'magento/package-2' => '1.0.1', + 'partner/package-3' => '3.0.0', + ] + ); + $composerInformation->expects($this->any()) + ->method('getRootPackage') + ->willReturn($rootPackage); + + return $composerInformation; } public function testSyncPackagesData() @@ -100,11 +216,69 @@ public function testSyncPackagesData() $this->assertArrayHasKey('date', $latestData['lastSyncDate']); $this->assertArrayHasKey('time', $latestData['lastSyncDate']); $this->assertArrayHasKey('packages', $latestData); - $this->assertSame(2, count($latestData['packages'])); - $this->assertSame(2, $latestData['countOfUpdate']); + $this->assertSame(3, count($latestData['packages'])); + $this->assertSame(3, $latestData['countOfUpdate']); $this->assertArrayHasKey('installPackages', $latestData); $this->assertSame(1, count($latestData['installPackages'])); $this->assertSame(1, $latestData['countOfInstall']); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Couldn't get available versions for package partner/package-4 + */ + public function testGetPackagesForUpdateWithException() + { + $requiredPackages = [ + 'partner/package-4' => '4.0.4', + ]; + $installedPackages = [ + 'partner/package-4' => [ + 'name' => 'partner/package-4', + 'type' => 'magento2-module', + 'version'=> '4.0.4' + ], + ]; + $this->composerInformation = $this->getComposerInformation($requiredPackages, $installedPackages); + $this->createPackagesData(); + $this->packagesData->getPackagesForUpdate(); + } + + public function testPackagesForUpdateFromJson() + { + $this->composerInformation = $this->getComposerInformation([], [], ['https://repo1']); + $this->packagesAuth->expects($this->atLeastOnce()) + ->method('getCredentialBaseUrl') + ->willReturn('repo1'); + $this->createPackagesData(); + $packages = $this->packagesData->getPackagesForUpdate(); + $this->assertEquals(2, count($packages)); + $this->assertArrayHasKey('magento/package-1', $packages); + $this->assertArrayHasKey('partner/package-3', $packages); + $firstPackage = array_values($packages)[0]; + $this->assertArrayHasKey('latestVersion', $firstPackage); + $this->assertArrayHasKey('versions', $firstPackage); + } + public function testGetPackagesForUpdate() + { + $packages = $this->packagesData->getPackagesForUpdate(); + $this->assertEquals(3, count($packages)); + $this->assertArrayHasKey('magento/package-1', $packages); + $this->assertArrayHasKey('magento/package-2', $packages); + $this->assertArrayHasKey('partner/package-3', $packages); + $firstPackage = array_values($packages)[0]; + $this->assertArrayHasKey('latestVersion', $firstPackage); + $this->assertArrayHasKey('versions', $firstPackage); } + + public function testGetInstalledPackages() + { + $installedPackages = $this->packagesData->getInstalledPackages(); + $this->assertEquals(3, count($installedPackages)); + $this->assertArrayHasKey('magento/package-1', $installedPackages); + $this->assertArrayHasKey('magento/package-2', $installedPackages); + $this->assertArrayHasKey('partner/package-3', $installedPackages); + } + } From 81b28db347828c24c94833bbdd6f2dcd98908317 Mon Sep 17 00:00:00 2001 From: Tim Nolan Date: Thu, 18 May 2017 13:50:24 -0500 Subject: [PATCH 089/363] Fix merge mistake --- .../src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php index ee1774bc187b7..916714d81d58f 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php @@ -5,8 +5,10 @@ */ namespace Magento\Setup\Test\Unit\Model; - -use \Magento\Setup\Model\PackagesData; +use Composer\Package\RootPackage; +use Magento\Framework\Composer\ComposerInformation; +use Magento\Setup\Model\PackagesData; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Tests Magento\Setup\Model\PackagesData From 6bf4083fb8dfbe20927660a50cb29399f9e8cca6 Mon Sep 17 00:00:00 2001 From: Tim Nolan Date: Thu, 18 May 2017 14:49:37 -0500 Subject: [PATCH 090/363] Remove uneeded code from develop --- .../Test/Unit/Model/PackagesDataTest.php | 264 +++--------------- 1 file changed, 44 insertions(+), 220 deletions(-) diff --git a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php index 916714d81d58f..96509f4ece2bd 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/PackagesDataTest.php @@ -5,10 +5,8 @@ */ namespace Magento\Setup\Test\Unit\Model; -use Composer\Package\RootPackage; -use Magento\Framework\Composer\ComposerInformation; -use Magento\Setup\Model\PackagesData; -use PHPUnit_Framework_MockObject_MockObject as MockObject; + +use \Magento\Setup\Model\PackagesData; /** * Tests Magento\Setup\Model\PackagesData @@ -20,96 +18,47 @@ class PackagesDataTest extends \PHPUnit_Framework_TestCase */ private $packagesData; - /** - * @var ComposerInformation|MockObject - */ - private $composerInformation; - - /** - * @var \Magento\Setup\Model\DateTime\TimeZoneProvider|MockObject - */ - private $timeZoneProvider; - - /** - * @var \Magento\Setup\Model\PackagesAuth|MockObject - */ - private $packagesAuth; - - /** - * @var \Magento\Framework\Filesystem|MockObject - */ - private $filesystem; - - /** - * @var \Magento\Setup\Model\ObjectManagerProvider|MockObject - */ - private $objectManagerProvider; - - /** - * @var \Magento\Setup\Model\Grid\TypeMapper|MockObject - */ - private $typeMapper; - public function setUp() { - $this->composerInformation = $this->getComposerInformation(); - $this->timeZoneProvider = $this->getMockBuilder(\Magento\Setup\Model\DateTime\TimeZoneProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $timeZone = $this->getMock(\Magento\Framework\Stdlib\DateTime\Timezone::class, [], [], '', false); - $this->timeZoneProvider->expects($this->any())->method('get')->willReturn($timeZone); - $this->packagesAuth = $this->getMock(\Magento\Setup\Model\PackagesAuth::class, [], [], '', false); - $this->filesystem = $this->getMock(\Magento\Framework\Filesystem::class, [], [], '', false); - $this->objectManagerProvider = $this->getMockBuilder(\Magento\Setup\Model\ObjectManagerProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $objectManager = $this->getMockForAbstractClass(\Magento\Framework\ObjectManagerInterface::class); - $appFactory = $this->getMockBuilder(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $application = $this->getMock(\Magento\Composer\MagentoComposerApplication::class, [], [], '', false); + $composerInformation = $this->getMock('\Magento\Framework\Composer\ComposerInformation', [], [], '', false); + $composerInformation->expects($this->any())->method('getInstalledMagentoPackages')->willReturn( + [ + ['name' => 'magento/package-1', 'type' => 'magento2-module', 'version'=> '1.0.0'], + ['name' => 'magento/package-2', 'type' => 'magento2-module', 'version'=> '1.0.1'] + ] + ); + + $composerInformation->expects($this->any())->method('getRootRepositories')->willReturn(['repo1', 'repo2']); + $composerInformation->expects($this->any())->method('getPackagesTypes')->willReturn(['magento2-module']); + $timeZoneProvider = $this->getMock('\Magento\Setup\Model\DateTime\TimeZoneProvider', [], [], '', false); + $timeZone = $this->getMock('\Magento\Framework\Stdlib\DateTime\Timezone', [], [], '', false); + $timeZoneProvider->expects($this->any())->method('get')->willReturn($timeZone); + $packagesAuth = $this->getMock('\Magento\Setup\Model\PackagesAuth', [], [], '', false); + $filesystem = $this->getMock('\Magento\Framework\Filesystem', [], [], '', false); + $objectManagerProvider = $this->getMock('\Magento\Setup\Model\ObjectManagerProvider', [], [], '', false); + $objectManager = $this->getMockForAbstractClass('\Magento\Framework\ObjectManagerInterface'); + $applicationFactory = $this->getMock( + '\Magento\Framework\Composer\MagentoComposerApplicationFactory', + [], + [], + '', + false + ); + $application = $this->getMock('\Magento\Composer\MagentoComposerApplication', [], [], '', false); $application->expects($this->any()) ->method('runComposerCommand') - ->willReturnMap([ - [ - [ - PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, - PackagesData::PARAM_PACKAGE => 'magento/package-1', - PackagesData::PARAM_AVAILABLE => true, - ], - null, - 'versions: 2.0.1' - ], - [ - [ - PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, - PackagesData::PARAM_PACKAGE => 'magento/package-2', - PackagesData::PARAM_AVAILABLE => true, - ], - null, - 'versions: 2.0.1' - ], - [ - [ - PackagesData::PARAM_COMMAND => PackagesData::COMPOSER_SHOW, - PackagesData::PARAM_PACKAGE => 'partner/package-3', - PackagesData::PARAM_AVAILABLE => true, - ], - null, - 'versions: 3.0.1' - ], - ]); - $appFactory->expects($this->any())->method('create')->willReturn($application); + ->willReturn('versions: 2.0.1'); + $applicationFactory->expects($this->any())->method('create')->willReturn($application); $objectManager->expects($this->any()) ->method('get') - ->with(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class) - ->willReturn($appFactory); - $this->objectManagerProvider->expects($this->any())->method('get')->willReturn($objectManager); - - $directoryWrite = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); - $directoryRead = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $this->filesystem->expects($this->any())->method('getDirectoryRead')->will($this->returnValue($directoryRead)); - $this->filesystem->expects($this->any()) + ->with('Magento\Framework\Composer\MagentoComposerApplicationFactory') + ->willReturn($applicationFactory); + $objectManagerProvider->expects($this->any())->method('get')->willReturn($objectManager); + + $directoryWrite = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\WriteInterface'); + $directoryRead = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\ReadInterface'); + $filesystem->expects($this->any())->method('getDirectoryRead')->will($this->returnValue($directoryRead)); + $filesystem->expects($this->any()) ->method('getDirectoryWrite') ->will($this->returnValue($directoryWrite)); $directoryWrite->expects($this->any())->method('isExist')->willReturn(true); @@ -135,80 +84,13 @@ public function setUp() . '}}}' ); - $this->typeMapper = $this->getMockBuilder(\Magento\Setup\Model\Grid\TypeMapper::class) - ->disableOriginalConstructor() - ->getMock(); - $this->typeMapper->expects(static::any()) - ->method('map') - ->willReturnMap([ - [ComposerInformation::MODULE_PACKAGE_TYPE, \Magento\Setup\Model\Grid\TypeMapper::MODULE_PACKAGE_TYPE], - ]); - - $this->createPackagesData(); - } - - private function createPackagesData() - { $this->packagesData = new PackagesData( - $this->composerInformation, - $this->timeZoneProvider, - $this->packagesAuth, - $this->filesystem, - $this->objectManagerProvider, - $this->typeMapper - ); - } - - /** - * @param array $requiredPackages - * @param array $installedPackages - * @param array $repo - * @return ComposerInformation|MockObject - */ - private function getComposerInformation($requiredPackages = [], $installedPackages = [], $repo = []) - { - $composerInformation = $this->getMock(ComposerInformation::class, [], [], '', false); - $composerInformation->expects($this->any())->method('getInstalledMagentoPackages')->willReturn( - $installedPackages ?: - [ - 'magento/package-1' => [ - 'name' => 'magento/package-1', - 'type' => 'magento2-module', - 'version'=> '1.0.0' - ], - 'magento/package-2' => [ - 'name' => 'magento/package-2', - 'type' => 'magento2-module', - 'version'=> '1.0.1' - ], - 'partner/package-3' => [ - 'name' => 'partner/package-3', - 'type' => 'magento2-module', - 'version'=> '3.0.0' - ], - ] + $composerInformation, + $timeZoneProvider, + $packagesAuth, + $filesystem, + $objectManagerProvider ); - - $composerInformation->expects($this->any())->method('getRootRepositories') - ->willReturn($repo ?: ['repo1', 'repo2']); - $composerInformation->expects($this->any())->method('getPackagesTypes') - ->willReturn(['magento2-module']); - $rootPackage = $this->getMock(RootPackage::class, [], ['magento/project', '2.1.0', '2']); - $rootPackage->expects($this->any()) - ->method('getRequires') - ->willReturn( - $requiredPackages ?: - [ - 'magento/package-1' => '1.0.0', - 'magento/package-2' => '1.0.1', - 'partner/package-3' => '3.0.0', - ] - ); - $composerInformation->expects($this->any()) - ->method('getRootPackage') - ->willReturn($rootPackage); - - return $composerInformation; } public function testSyncPackagesData() @@ -218,69 +100,11 @@ public function testSyncPackagesData() $this->assertArrayHasKey('date', $latestData['lastSyncDate']); $this->assertArrayHasKey('time', $latestData['lastSyncDate']); $this->assertArrayHasKey('packages', $latestData); - $this->assertSame(3, count($latestData['packages'])); - $this->assertSame(3, $latestData['countOfUpdate']); + $this->assertSame(2, count($latestData['packages'])); + $this->assertSame(2, $latestData['countOfUpdate']); $this->assertArrayHasKey('installPackages', $latestData); $this->assertSame(1, count($latestData['installPackages'])); $this->assertSame(1, $latestData['countOfInstall']); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Couldn't get available versions for package partner/package-4 - */ - public function testGetPackagesForUpdateWithException() - { - $requiredPackages = [ - 'partner/package-4' => '4.0.4', - ]; - $installedPackages = [ - 'partner/package-4' => [ - 'name' => 'partner/package-4', - 'type' => 'magento2-module', - 'version'=> '4.0.4' - ], - ]; - $this->composerInformation = $this->getComposerInformation($requiredPackages, $installedPackages); - $this->createPackagesData(); - $this->packagesData->getPackagesForUpdate(); - } - - public function testPackagesForUpdateFromJson() - { - $this->composerInformation = $this->getComposerInformation([], [], ['https://repo1']); - $this->packagesAuth->expects($this->atLeastOnce()) - ->method('getCredentialBaseUrl') - ->willReturn('repo1'); - $this->createPackagesData(); - $packages = $this->packagesData->getPackagesForUpdate(); - $this->assertEquals(2, count($packages)); - $this->assertArrayHasKey('magento/package-1', $packages); - $this->assertArrayHasKey('partner/package-3', $packages); - $firstPackage = array_values($packages)[0]; - $this->assertArrayHasKey('latestVersion', $firstPackage); - $this->assertArrayHasKey('versions', $firstPackage); - } - public function testGetPackagesForUpdate() - { - $packages = $this->packagesData->getPackagesForUpdate(); - $this->assertEquals(3, count($packages)); - $this->assertArrayHasKey('magento/package-1', $packages); - $this->assertArrayHasKey('magento/package-2', $packages); - $this->assertArrayHasKey('partner/package-3', $packages); - $firstPackage = array_values($packages)[0]; - $this->assertArrayHasKey('latestVersion', $firstPackage); - $this->assertArrayHasKey('versions', $firstPackage); } - - public function testGetInstalledPackages() - { - $installedPackages = $this->packagesData->getInstalledPackages(); - $this->assertEquals(3, count($installedPackages)); - $this->assertArrayHasKey('magento/package-1', $installedPackages); - $this->assertArrayHasKey('magento/package-2', $installedPackages); - $this->assertArrayHasKey('partner/package-3', $installedPackages); - } - } From 9a653468c26bf8e03b01102176bd0913fd1dfc3e Mon Sep 17 00:00:00 2001 From: Tim Nolan Date: Thu, 18 May 2017 15:29:57 -0500 Subject: [PATCH 091/363] Removed unused method --- .../src/Magento/Setup/Model/PackagesData.php | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/setup/src/Magento/Setup/Model/PackagesData.php b/setup/src/Magento/Setup/Model/PackagesData.php index 055cb53e6af71..d8e893a5c4153 100644 --- a/setup/src/Magento/Setup/Model/PackagesData.php +++ b/setup/src/Magento/Setup/Model/PackagesData.php @@ -325,31 +325,6 @@ public function getPackagesForInstall() } } - /** - * Filter packages by allowed types - * - * @param array $packages - * @return array - */ - private function filterPackagesList(array $packages) - { - return array_filter( - $packages, - function ($item) { - return in_array( - $item['package_type'], - [ - \Magento\Setup\Model\Grid\TypeMapper::LANGUAGE_PACKAGE_TYPE, - \Magento\Setup\Model\Grid\TypeMapper::MODULE_PACKAGE_TYPE, - \Magento\Setup\Model\Grid\TypeMapper::EXTENSION_PACKAGE_TYPE, - \Magento\Setup\Model\Grid\TypeMapper::THEME_PACKAGE_TYPE, - \Magento\Setup\Model\Grid\TypeMapper::METAPACKAGE_PACKAGE_TYPE - ] - ); - } - ); - } - /** * Get MetaPackage for package * From 0ccc53fec29870ddb193f71a1c5e09758d9eba2b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 08:48:41 +0300 Subject: [PATCH 092/363] MAGETWO-64068: [Backport] - Not possible to update or delete products in cart or checkout after deleting address - for 2.1 --- .../ShippingAssignmentProcessor.php | 44 ++- .../Model/QuoteRepository/SaveHandler.php | 39 ++- .../ShippingAssignmentProcessorTest.php | 210 ++++++++++---- .../Model/QuoteRepository/SaveHandlerTest.php | 258 ++++++++++++------ .../Quote/Model/QuoteRepositoryTest.php | 132 +++++++-- 5 files changed, 513 insertions(+), 170 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php b/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php index fdf819c8d864c..45d55a4be794f 100644 --- a/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php +++ b/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php @@ -9,7 +9,10 @@ use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Framework\Exception\InputException; use Magento\Quote\Model\ShippingAssignmentFactory; -use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\ObjectManager; class ShippingAssignmentProcessor { @@ -24,45 +27,65 @@ class ShippingAssignmentProcessor protected $shippingProcessor; /** - * @var CartItemPersister + * @var \Magento\Quote\Model\Quote\Item\CartItemPersister */ protected $cartItemPersister; + /** + * Customer address CRUD interface. + * + * @var AddressRepositoryInterface + */ + private $addressRepository; + /** * @param ShippingAssignmentFactory $shippingAssignmentFactory * @param ShippingProcessor $shippingProcessor - * @param CartItemPersister $cartItemPersister + * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister + * @param AddressRepositoryInterface $addressRepository */ public function __construct( ShippingAssignmentFactory $shippingAssignmentFactory, ShippingProcessor $shippingProcessor, - CartItemPersister $cartItemPersister + \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, + AddressRepositoryInterface $addressRepository = null ) { $this->shippingAssignmentFactory = $shippingAssignmentFactory; $this->shippingProcessor = $shippingProcessor; $this->cartItemPersister = $cartItemPersister; + $this->addressRepository = $addressRepository + ?: ObjectManager::getInstance()->get(AddressRepositoryInterface::class); } /** + * Create shipping assignment. + * * @param CartInterface $quote + * * @return \Magento\Quote\Api\Data\ShippingAssignmentInterface */ public function create(CartInterface $quote) { /** @var \Magento\Quote\Model\Quote $quote */ $shippingAddress = $quote->getShippingAddress(); + /** @var \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment */ $shippingAssignment = $this->shippingAssignmentFactory->create(); $shippingAssignment->setItems($quote->getItems()); $shippingAssignment->setShipping($this->shippingProcessor->create($shippingAddress)); + return $shippingAssignment; } /** + * Save shipping assignment. + * * @param ShippingAssignmentInterface $shippingAssignment * @param CartInterface $quote + * * @return void - * @throws InputException + * + * @throws InputException|LocalizedException */ public function save(CartInterface $quote, ShippingAssignmentInterface $shippingAssignment) { @@ -73,6 +96,17 @@ public function save(CartInterface $quote, ShippingAssignmentInterface $shipping $this->cartItemPersister->save($quote, $item); } } + + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + + if ($shippingAddress->getCustomerAddressId()) { + try { + $this->addressRepository->getById($shippingAddress->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $shippingAddress->setCustomerAddressId(null); + } + } + $this->shippingProcessor->save($shippingAssignment->getShipping(), $quote); } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php index 647946e141a63..e91becaf3cf88 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php @@ -7,6 +7,8 @@ use Magento\Quote\Api\Data\CartInterface; use Magento\Framework\Exception\InputException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; class SaveHandler { @@ -30,26 +32,40 @@ class SaveHandler */ private $shippingAssignmentPersister; + /** + * Customer address CRUD interface. + * + * @var \Magento\Customer\Api\AddressRepositoryInterface + */ + private $addressRepository; + /** * @param \Magento\Quote\Model\ResourceModel\Quote $quoteResource * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister * @param \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister * @param \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister + * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository */ public function __construct( \Magento\Quote\Model\ResourceModel\Quote $quoteResource, \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister, - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister + \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister, + \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null ) { $this->quoteResourceModel = $quoteResource; $this->cartItemPersister = $cartItemPersister; $this->billingAddressPersister = $billingAddressPersister; $this->shippingAssignmentPersister = $shippingAssignmentPersister; + $this->addressRepository = $addressRepository + ?: ObjectManager::getInstance()->get(\Magento\Customer\Api\AddressRepositoryInterface::class); } /** + * Process and save quote data. + * * @param CartInterface $quote + * * @return CartInterface * * @throws InputException @@ -62,6 +78,7 @@ public function save(CartInterface $quote) /** @var \Magento\Quote\Model\Quote $quote */ // Quote Item processing $items = $quote->getItems(); + if ($items) { foreach ($items as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ @@ -73,30 +90,46 @@ public function save(CartInterface $quote) // Billing Address processing $billingAddress = $quote->getBillingAddress(); + if ($billingAddress) { + if ($billingAddress->getCustomerAddressId()) { + try { + $this->addressRepository->getById($billingAddress->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $billingAddress->setCustomerAddressId(null); + } + } + $this->billingAddressPersister->save($quote, $billingAddress); } $this->processShippingAssignment($quote); - $this->quoteResourceModel->save($quote->collectTotals()); + return $quote; } /** + * Process shipping assignment. + * * @param \Magento\Quote\Model\Quote $quote + * * @return void + * * @throws InputException */ private function processShippingAssignment($quote) { // Shipping Assignments processing $extensionAttributes = $quote->getExtensionAttributes(); + if (!$quote->isVirtual() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { $shippingAssignments = $extensionAttributes->getShippingAssignments(); + if (count($shippingAssignments) > 1) { - throw new InputException(__("Only 1 shipping assignment can be set")); + throw new InputException(__('Only 1 shipping assignment can be set')); } + $this->shippingAssignmentPersister->save($quote, $shippingAssignments[0]); } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php index e74070c3eaee7..2f1b1c093604e 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php @@ -5,107 +5,199 @@ */ namespace Magento\Quote\Test\Unit\Model\Quote\ShippingAssignment; -use Magento\Quote\Api\Data\ShippingAssignmentInterface; -use Magento\Quote\Api\Data\ShippingInterface; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\ShippingAssignmentFactory; -use Magento\Quote\Model\Quote\Item\CartItemPersister; use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Quote\Model\ShippingAssignmentFactory; use Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Framework\Exception\NoSuchEntityException; /** - * Class ShippingAssignmentProcessorTest + * ShippingAssignmentProcessor test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShippingAssignmentProcessorTest extends \PHPUnit_Framework_TestCase { /** - * @var ShippingAssignmentFactory|MockObject + * @var ShippingAssignmentProcessor */ - private $shippingAssignmentFactory; + private $shippingAssignmentProcessor; /** - * @var ShippingProcessor|MockObject + * @var ObjectManagerHelper */ - private $shippingProcessor; + private $objectManagerHelper; /** - * @var CartItemPersister|MockObject + * @var ShippingAssignmentFactory|\PHPUnit_Framework_MockObject_MockObject */ - private $cartItemPersister; + private $shippingAssignmentFactoryMock; /** - * @var ShippingAssignmentProcessor + * @var ShippingProcessor|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentProcessor; + private $shippingProcessorMock; + + /** + * @var CartItemPersister|\PHPUnit_Framework_MockObject_MockObject + */ + private $cartItemPersisterMock; + + /** + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepositoryMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var ShippingAssignmentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingAssignmentMock; + + /** + * @var QuoteAddress|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingAddressMock; + + /** + * @var ShippingInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingMock; protected function setUp() { - $this->shippingAssignmentFactory = $this->getMockBuilder(ShippingAssignmentFactory::class) + $this->shippingAssignmentFactoryMock = $this->getMockBuilder(ShippingAssignmentFactory::class) ->disableOriginalConstructor() ->getMock(); - - $this->shippingProcessor = $this->getMockBuilder(ShippingProcessor::class) + $this->shippingProcessorMock = $this->getMockBuilder(ShippingProcessor::class) ->disableOriginalConstructor() ->getMock(); - - $this->cartItemPersister = $this->getMockBuilder(CartItemPersister::class) + $this->cartItemPersisterMock = $this->getMockBuilder(CartItemPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->addressRepositoryMock = $this->getMockBuilder(AddressRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shippingAssignmentMock = $this->getMockBuilder(ShippingAssignmentInterface::class) + ->getMockForAbstractClass(); + $this->shippingAddressMock = $this->getMockBuilder(QuoteAddress::class) ->disableOriginalConstructor() ->getMock(); + $this->shippingMock = $this->getMockBuilder(ShippingInterface::class) + ->getMockForAbstractClass(); + + $this->quoteMock->expects(static::any()) + ->method('getShippingAddress') + ->willReturn($this->shippingAddressMock); + $this->shippingAssignmentMock->expects(static::any()) + ->method('getShipping') + ->willReturn($this->shippingMock); + $this->shippingMock->expects(static::any()) + ->method('getAddress') + ->willReturn($this->shippingAddressMock); - $this->shippingAssignmentProcessor = new ShippingAssignmentProcessor( - $this->shippingAssignmentFactory, - $this->shippingProcessor, - $this->cartItemPersister + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->shippingAssignmentProcessor = $this->objectManagerHelper->getObject( + ShippingAssignmentProcessor::class, + [ + 'shippingAssignmentFactory' => $this->shippingAssignmentFactoryMock, + 'shippingProcessor' => $this->shippingProcessorMock, + 'cartItemPersister' => $this->cartItemPersisterMock, + 'addressRepository' => $this->addressRepositoryMock + ] ); } /** - * Test saving shipping assignments with deleted cart items - * - * @covers \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor::save + * Tests save() method with deleted cart items. */ public function testSaveWithDeletedCartItems() { - $shippingAssignment = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); - $shipping = $this->getMockForAbstractClass(ShippingInterface::class); - $quoteId = 1; + $quoteItemId = 1; - $quote = $this->getMockBuilder(Quote::class) - ->disableOriginalConstructor() - ->getMock(); - $quoteItem = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $quoteItem->expects(static::once()) - ->method('isDeleted') - ->willReturn(true); - $quoteItem->expects(static::once()) - ->method('getItemId') - ->willReturn($quoteId); - - $quote->expects(static::once()) + $this->shippingAssignmentMock->expects(static::once()) + ->method('getItems') + ->willReturn([$this->createQuoteItemMock($quoteItemId, true)]); + $this->quoteMock->expects(static::atLeastOnce()) ->method('getItemById') - ->with($quoteId) + ->with($quoteItemId) ->willReturn(null); + $this->cartItemPersisterMock->expects(static::never()) + ->method('save'); + $this->shippingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn(null); + $this->addressRepositoryMock->expects(static::never()) + ->method('getById'); + $this->shippingProcessorMock->expects(static::once()) + ->method('save') + ->with($this->shippingMock, $this->quoteMock); - $shippingAssignment->expects(static::once()) - ->method('getItems') - ->willReturn([$quoteItem]); - $shippingAssignment->expects(static::once()) - ->method('getShipping') - ->willReturn($shipping); + $this->shippingAssignmentProcessor->save($this->quoteMock, $this->shippingAssignmentMock); + } - $this->cartItemPersister->expects(static::never()) - ->method('save'); + /** + * Tests save() method with not existing customer address. + */ + public function testSaveWithNotExistingCustomerAddress() + { + $customerAddressId = 11; - $this->shippingProcessor->expects(static::once()) + $this->shippingAssignmentMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([]); + $this->shippingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn($customerAddressId); + $this->addressRepositoryMock->expects(static::once()) + ->method('getById') + ->with($customerAddressId) + ->willThrowException(new NoSuchEntityException()); + $this->shippingAddressMock->expects(static::once()) + ->method('setCustomerAddressId') + ->with(null) + ->willReturn($this->shippingAddressMock); + $this->shippingProcessorMock->expects(static::once()) ->method('save') - ->with($shipping, $quote); + ->with($this->shippingMock, $this->quoteMock); - $this->shippingAssignmentProcessor->save( - $quote, - $shippingAssignment - ); + $this->shippingAssignmentProcessor->save($this->quoteMock, $this->shippingAssignmentMock); + } + + /** + * Create quote item mock. + * + * @param int|string $id + * @param bool $isDeleted + * + * @return QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private function createQuoteItemMock($id, $isDeleted) + { + $quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItemMock->expects(static::any()) + ->method('getItemId') + ->willReturn($id); + $quoteItemMock->expects(static::any()) + ->method('isDeleted') + ->willReturn($isDeleted); + + return $quoteItemMock; } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php index 633beb6c1853a..86bb3ddcf3386 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php @@ -3,11 +3,26 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Test\Unit\Model\QuoteRepository; use Magento\Quote\Model\QuoteRepository\SaveHandler; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResourceModel; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Quote\Model\Quote\Address\BillingAddressPersister; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Framework\Exception\NoSuchEntityException; +/** + * SaveHandler test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveHandlerTest extends \PHPUnit_Framework_TestCase { /** @@ -16,124 +31,201 @@ class SaveHandlerTest extends \PHPUnit_Framework_TestCase private $saveHandler; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ObjectManagerHelper */ - private $cartItemPersister; + private $objectManagerHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var QuoteResourceModel|\PHPUnit_Framework_MockObject_MockObject */ - private $billingAddressPersister; + private $quoteResourceModelMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartItemPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteResourceModel; + private $cartItemPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var BillingAddressPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentPersister; + private $billingAddressPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ShippingAssignmentPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteMock; + private $shippingAssignmentPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $itemMock; + private $addressRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ - private $billingAddressMock; + private $quoteMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var QuoteAddress|\PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttributeMock; + private $billingAddressMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartExtensionInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentMock; + private $extensionAttributesMock; protected function setUp() { - $this->quoteResourceModel = $this->getMock(\Magento\Quote\Model\ResourceModel\Quote::class, [], [], '', false); - $this->cartItemPersister = $this->getMock( - \Magento\Quote\Model\Quote\Item\CartItemPersister::class, - [], - [], - '', - false - ); - $this->billingAddressPersister = $this->getMock( - \Magento\Quote\Model\Quote\Address\BillingAddressPersister::class, - [], - [], - '', - false - ); - $this->shippingAssignmentPersister = $this->getMock( - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister::class, - [], - [], - '', - false - ); - $methods = [ - 'getItems', 'setLastAddedItem', 'getBillingAddress', 'getIsActive', - 'getExtensionAttributes', 'isVirtual', 'collectTotals' - ]; - $this->quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, $methods, [], '', false); - $this->itemMock = $this->getMock(\Magento\Quote\Model\Quote\Item::class, [], [], '', false); - $this->billingAddressMock = $this->getMock(\Magento\Quote\Model\Quote\Address::class, [], [], '', false); - $this->extensionAttributeMock = $this->getMock(\Magento\Quote\Api\Data\CartExtensionInterface::class); - $this->shippingAssignmentMock = - $this->getMock( - \Magento\Quote\Api\Data\CartExtension::class, - ['getShippingAssignments', 'setShippingAssignments'], - [], - '', - false - ); - $this->saveHandler = new SaveHandler( - $this->quoteResourceModel, - $this->cartItemPersister, - $this->billingAddressPersister, - $this->shippingAssignmentPersister - ); + $this->quoteResourceModelMock = $this->getMockBuilder(QuoteResourceModel::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cartItemPersisterMock = $this->getMockBuilder(CartItemPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->billingAddressPersisterMock = $this->getMockBuilder(BillingAddressPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shippingAssignmentPersisterMock = $this->getMockBuilder(ShippingAssignmentPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->addressRepositoryMock = $this->getMockBuilder(AddressRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getItems', 'setLastAddedItem', 'getBillingAddress', 'getExtensionAttributes', 'isVirtual', + 'collectTotals' + ] + ) + ->getMock(); + $this->billingAddressMock = $this->getMockBuilder(QuoteAddress::class) + ->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder(CartExtensionInterface::class) + ->getMockForAbstractClass(); + + $this->quoteMock->expects(static::any()) + ->method('getBillingAddress') + ->willReturn($this->billingAddressMock); + $this->quoteMock->expects(static::any()) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->saveHandler = $this->objectManagerHelper->getObject( + SaveHandler::class, + [ + 'quoteResource' => $this->quoteResourceModelMock, + 'cartItemPersister' => $this->cartItemPersisterMock, + 'billingAddressPersister' => $this->billingAddressPersisterMock, + 'shippingAssignmentPersister' => $this->shippingAssignmentPersisterMock, + 'addressRepository' => $this->addressRepositoryMock + ] + ); } + /** + * Tests save() method for virtual quote. + */ public function testSaveForVirtualQuote() { - $this->quoteMock->expects($this->once())->method('getItems')->willReturn([$this->itemMock]); - $this->itemMock->expects($this->once())->method('isDeleted')->willReturn(false); - $this->cartItemPersister - ->expects($this->once()) + $quoteItemMock = $this->createQuoteItemMock(false); + + $this->quoteMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([$quoteItemMock]); + $this->cartItemPersisterMock->expects(static::once()) ->method('save') - ->with($this->quoteMock, $this->itemMock) - ->willReturn($this->itemMock); - $this->quoteMock->expects($this->once())->method('setLastAddedItem')->with($this->itemMock); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($this->billingAddressMock); - $this->billingAddressPersister - ->expects($this->once()) + ->with($this->quoteMock, $quoteItemMock) + ->willReturn($quoteItemMock); + $this->quoteMock->expects(static::once()) + ->method('setLastAddedItem') + ->with($quoteItemMock) + ->willReturnSelf(); + $this->billingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn(null); + $this->billingAddressMock->expects(static::never()) + ->method('getCustomerAddress'); + $this->billingAddressPersisterMock->expects(static::once()) ->method('save') ->with($this->quoteMock, $this->billingAddressMock); - $this->quoteMock - ->expects($this->once()) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributeMock); - $this->extensionAttributeMock - ->expects($this->never()) + $this->quoteMock->expects(static::atLeastOnce()) + ->method('isVirtual') + ->willReturn(true); + $this->extensionAttributesMock->expects(static::never()) + ->method('getShippingAssignments'); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('collectTotals') + ->willReturnSelf(); + $this->quoteResourceModelMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock) + ->willReturnSelf(); + + $this->assertSame($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + } + + /** + * Tests save() method with not existing customer address. + */ + public function testSaveWithNotExistingCustomerAddress() + { + $customerAddressId = 5; + + $this->quoteMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([]); + $this->quoteMock->expects(static::never()) + ->method('setLastAddedItem'); + $this->billingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn($customerAddressId); + $this->addressRepositoryMock->expects(static::once()) + ->method('getById') + ->with($customerAddressId) + ->willThrowException(new NoSuchEntityException()); + $this->billingAddressMock->expects(static::once()) + ->method('setCustomerAddressId') + ->willReturn(null); + $this->billingAddressPersisterMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock, $this->billingAddressMock); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('isVirtual') + ->willReturn(true); + $this->extensionAttributesMock->expects(static::never()) ->method('getShippingAssignments'); - $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(true); - $this->quoteMock->expects($this->once())->method('collectTotals')->willReturn($this->quoteMock); - $this->quoteResourceModel->expects($this->once())->method('save')->with($this->quoteMock); - $this->assertEquals($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('collectTotals') + ->willReturnSelf(); + $this->quoteResourceModelMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock) + ->willReturnSelf(); + + $this->assertSame($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + } + + /** + * Create quote item mock. + * + * @param bool $isDeleted + * + * @return QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private function createQuoteItemMock($isDeleted) + { + $quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItemMock->expects(static::any()) + ->method('isDeleted') + ->willReturn($isDeleted); + + return $quoteItemMock; } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 3a25f419e38ba..103c72bc237c4 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -7,25 +7,62 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Api\FilterBuilder; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Quote\Api\Data\CartInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\User\Api\Data\UserInterface; +use Magento\Framework\Api\SearchResults; +use Magento\Quote\Api\Data\CartSearchResultsInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\CartExtension; +/** + * QuoteRepository test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class QuoteRepositoryTest extends \PHPUnit_Framework_TestCase { /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->quoteRepository = $this->objectManager->create(QuoteRepository::class); + $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + } + /** + * Tests getting list of quotes according to search criteria. * @magentoDataFixture Magento/Sales/_files/quote.php */ public function testGetList() { $searchCriteria = $this->getSearchCriteria('test01'); - /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ - $quoteRepository = Bootstrap::getObjectManager()->create(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria); + $searchResult = $this->quoteRepository->getList($searchCriteria); $this->performAssertions($searchResult); } /** + * Tests getting list of quotes according to different search criterias. * @magentoDataFixture Magento/Sales/_files/quote.php */ public function testGetListDoubleCall() @@ -33,37 +70,91 @@ public function testGetListDoubleCall() $searchCriteria1 = $this->getSearchCriteria('test01'); $searchCriteria2 = $this->getSearchCriteria('test02'); - /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ - $quoteRepository = Bootstrap::getObjectManager()->create(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria1); + $searchResult = $this->quoteRepository->getList($searchCriteria1); $this->performAssertions($searchResult); - $searchResult = $quoteRepository->getList($searchCriteria2); - $items = $searchResult->getItems(); - $this->assertEmpty($items); + + $searchResult = $this->quoteRepository->getList($searchCriteria2); + $this->assertEmpty($searchResult->getItems()); + } + + /** + * Save quote test. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testSaveWithNotExistingCustomerAddress() + { + $addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; + + /** @var QuoteAddress $billingAddress */ + $billingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING) + ->setCustomerAddressId('not_existing'); + + /** @var QuoteAddress $shippingAddress */ + $shippingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); + $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING) + ->setCustomerAddressId('not_existing'); + + /** @var Shipping $shipping */ + $shipping = $this->objectManager->create(Shipping::class); + $shipping->setAddress($shippingAddress); + + /** @var ShippingAssignment $shippingAssignment */ + $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shippingAssignment->setItems([]); + $shippingAssignment->setShipping($shipping); + + /** @var CartExtension $extensionAttributes */ + $extensionAttributes = $this->objectManager->create(CartExtension::class); + $extensionAttributes->setShippingAssignments([$shippingAssignment]); + + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setExtensionAttributes($extensionAttributes) + ->save(); + $this->quoteRepository->save($quote); + + $this->assertNull($quote->getBillingAddress()->getCustomerAddressId()); + $this->assertNull( + $quote->getExtensionAttributes() + ->getShippingAssignments()[0] + ->getShipping() + ->getAddress() + ->getCustomerAddressId() + ); } /** + * Get search criteria. + * * @param string $filterValue + * * @return \Magento\Framework\Api\SearchCriteria */ private function getSearchCriteria($filterValue) { - /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); - $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $filters = []; - $filters[] = $filterBuilder + $filters[] = $this->filterBuilder ->setField('reserved_order_id') ->setConditionType('=') ->setValue($filterValue) ->create(); - $searchCriteriaBuilder->addFilters($filters); + $this->searchCriteriaBuilder->addFilters($filters); - return $searchCriteriaBuilder->create(); + return $this->searchCriteriaBuilder->create(); } /** - * @param object $searchResult + * Perform assertions. + * + * @param SearchResults|CartSearchResultsInterface $searchResult */ protected function performAssertions($searchResult) { @@ -74,12 +165,13 @@ protected function performAssertions($searchResult) ]; $items = $searchResult->getItems(); - /** @var \Magento\Quote\Api\Data\CartInterface $actualQuote */ + /** @var CartInterface $actualQuote */ $actualQuote = array_pop($items); + /** @var UserInterface $testAttribute */ + $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); + $this->assertInstanceOf(CartInterface::class, $actualQuote); $this->assertEquals('test01', $actualQuote->getReservedOrderId()); - /** @var \Magento\User\Api\Data\UserInterface $testAttribute */ - $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute->getFirstName()); $this->assertEquals($expectedExtensionAttributes['lastname'], $testAttribute->getLastName()); $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute->getEmail()); From ea25325b0b9f23ea63c9b3351f8f89bbb482bc5b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 09:13:16 +0300 Subject: [PATCH 093/363] MAGETWO-64068: [Backport] - Not possible to update or delete products in cart or checkout after deleting address - for 2.1 --- .../testsuite/Magento/Quote/Model/QuoteRepositoryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 103c72bc237c4..104b0162121b9 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -50,6 +50,7 @@ protected function setUp() $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); } + /** * Tests getting list of quotes according to search criteria. * @magentoDataFixture Magento/Sales/_files/quote.php From 9ff16b00276e442b59a007dce3ffad9bd4f1188d Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 11:58:43 +0300 Subject: [PATCH 094/363] MAGETWO-64730: [Backport] - Cannot place order with PayPal Express (last product in stock) - for 2.1 --- .../Magento/Paypal/Model/Express/Checkout.php | 38 ++++++- .../Paypal/Model/Express/CheckoutTest.php | 98 ++++++++++++++++--- .../Magento/Paypal/_files/quote_express.php | 90 +++++++++++++++++ .../_files/quote_express_with_customer.php | 77 +++++++++++++++ .../Paypal/_files/quote_payment_express.php | 92 +---------------- .../quote_payment_express_with_customer.php | 74 +------------- 6 files changed, 291 insertions(+), 178 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php create mode 100644 dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index be185ce7b0ac0..a35a5eea96a6b 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -12,6 +12,8 @@ use Magento\Quote\Model\Quote\Address; use Magento\Framework\DataObject; use Magento\Paypal\Model\Cart as PaypalCart; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Wrapper that performs Paypal Express and Checkout communication @@ -70,7 +72,7 @@ class Checkout * * @var string */ - protected $_apiType = 'Magento\Paypal\Model\Api\Nvp'; + protected $_apiType = \Magento\Paypal\Model\Api\Nvp::class; /** * Payment method type @@ -268,6 +270,13 @@ class Checkout */ protected $totalsCollector; + /** + * Order repository interface. + * + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Customer\Model\Url $customerUrl @@ -538,6 +547,12 @@ public function start($returnUrl, $cancelUrl, $button = null) } $this->_api->setSuppressShipping(true); } else { + $billingAddress = $this->_quote->getBillingAddress(); + + if ($billingAddress) { + $this->_api->setBillingAddress($billingAddress); + } + $address = $this->_quote->getShippingAddress(); $isOverridden = 0; if (true === $address->validate()) { @@ -668,6 +683,7 @@ public function returnFromPaypal($token) $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); $billingAddress->setCustomerNote($exportedBillingAddress->getData('note')); $quote->setBillingAddress($billingAddress); + $quote->setCheckoutMethod($this->getCheckoutMethod()); // import payment info $payment = $quote->getPayment(); @@ -789,7 +805,8 @@ public function place($token, $shippingMethodCode = null) $this->ignoreAddressValidation(); $this->_quote->collectTotals(); - $order = $this->quoteManagement->submit($this->_quote); + $orderId = $this->quoteManagement->placeOrder($this->_quote->getId()); + $order = $this->getOrderRepository()->get($orderId); if (!$order) { return; @@ -1157,4 +1174,21 @@ protected function prepareGuestQuote() ->setCustomerGroupId(\Magento\Customer\Model\Group::NOT_LOGGED_IN_ID); return $this; } + + /** + * Returns order repository instance. + * + * @return OrderRepositoryInterface + * + * @deprecated + */ + private function getOrderRepository() + { + if ($this->orderRepository === null) { + $this->orderRepository = ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); + } + + return $this->orderRepository; + } } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index bf1fed29857c6..0fe79ad7bd2db 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -8,6 +8,10 @@ use Magento\Quote\Model\Quote; use Magento\Checkout\Model\Type\Onepage; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Paypal\Model\Config; +use Magento\Paypal\Model\Api\Type\Factory; +use Magento\Paypal\Model\Info; +use Magento\Paypal\Model\Api\Nvp; /** * Class CheckoutTest @@ -32,7 +36,7 @@ protected function setUp() /** * Verify that an order placed with an existing customer can re-use the customer addresses. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoDataFixture Magento/Paypal/_files/quote_express_with_customer.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -43,19 +47,19 @@ public function testPrepareCustomerQuote() $quote->setCheckoutMethod(Onepage::METHOD_CUSTOMER); // to dive into _prepareCustomerQuote() on switch $quote->getShippingAddress()->setSameAsBilling(0); $quote->setReservedOrderId(null); - $customer = $this->_objectManager->create('Magento\Customer\Model\Customer')->load(1); + $customer = $this->_objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); $customer->setDefaultBilling(false) ->setDefaultShipping(false) ->save(); /** @var \Magento\Customer\Model\Session $customerSession */ - $customerSession = $this->_objectManager->get('Magento\Customer\Model\Session'); + $customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); $customerSession->loginById(1); $checkout = $this->_getCheckout($quote); $checkout->place('token'); /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerService */ - $customerService = $this->_objectManager->get('Magento\Customer\Api\CustomerRepositoryInterface'); + $customerService = $this->_objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); $customer = $customerService->getById($quote->getCustomerId()); $this->assertEquals(1, $quote->getCustomerId()); @@ -72,7 +76,7 @@ public function testPrepareCustomerQuote() /** * Verify that after placing the order, addresses are associated with the order and the quote is a guest quote. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php + * @magentoDataFixture Magento/Paypal/_files/quote_express.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -109,10 +113,10 @@ public function testPlaceGuestQuote() protected function _getCheckout(Quote $quote) { return $this->_objectManager->create( - 'Magento\Paypal\Model\Express\Checkout', + \Magento\Paypal\Model\Express\Checkout::class, [ 'params' => [ - 'config' => $this->getMock('Magento\Paypal\Model\Config', [], [], '', false), + 'config' => $this->getMock(\Magento\Paypal\Model\Config::class, [], [], '', false), 'quote' => $quote, ] ] @@ -129,11 +133,11 @@ protected function _getCheckout(Quote $quote) public function testReturnFromPaypal() { $quote = $this->_getFixtureQuote(); - $paypalConfigMock = $this->getMock('Magento\Paypal\Model\Config', [], [], '', false); - $apiTypeFactory = $this->getMock('Magento\Paypal\Model\Api\Type\Factory', [], [], '', false); - $paypalInfo = $this->getMock('Magento\Paypal\Model\Info', [], [], '', false); + $paypalConfigMock = $this->getMock(\Magento\Paypal\Model\Config::class, [], [], '', false); + $apiTypeFactory = $this->getMock(\Magento\Paypal\Model\Api\Type\Factory::class, [], [], '', false); + $paypalInfo = $this->getMock(\Magento\Paypal\Model\Info::class, [], [], '', false); $checkoutModel = $this->_objectManager->create( - 'Magento\Paypal\Model\Express\Checkout', + \Magento\Paypal\Model\Express\Checkout::class, [ 'params' => ['quote' => $quote, 'config' => $paypalConfigMock], 'apiTypeFactory' => $apiTypeFactory, @@ -142,7 +146,7 @@ public function testReturnFromPaypal() ); $api = $this->getMock( - 'Magento\Paypal\Model\Api\Nvp', + \Magento\Paypal\Model\Api\Nvp::class, ['call', 'getExportedShippingAddress', 'getExportedBillingAddress'], [], '', @@ -190,7 +194,73 @@ public function testReturnFromPaypal() } /** - * Prepare fixture for exported address + * Verify that guest customer quote has set type of checkout. + * + * @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testGuestReturnFromPaypal() + { + $quote = $this->_getFixtureQuote(); + $paypalConfig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + + $apiTypeFactory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $paypalInfo = $this->getMockBuilder(Info::class) + ->disableOriginalConstructor() + ->setMethods(['importToPayment']) + ->getMock(); + + $checkoutModel = $this->_objectManager->create( + Checkout::class, + [ + 'params' => ['quote' => $quote, 'config' => $paypalConfig], + 'apiTypeFactory' => $apiTypeFactory, + 'paypalInfo' => $paypalInfo + ] + ); + + $api = $this->getMockBuilder(Nvp::class) + ->disableOriginalConstructor() + ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress']) + ->getMock(); + + $api->expects($this->any()) + ->method('call') + ->will($this->returnValue([])); + + $apiTypeFactory->expects($this->any()) + ->method('create') + ->will($this->returnValue($api)); + + $exportedBillingAddress = $this->_getExportedAddressFixture($quote->getBillingAddress()->getData()); + $api->expects($this->any()) + ->method('getExportedBillingAddress') + ->will($this->returnValue($exportedBillingAddress)); + + $exportedShippingAddress = $this->_getExportedAddressFixture($quote->getShippingAddress()->getData()); + $api->expects($this->any()) + ->method('getExportedShippingAddress') + ->will($this->returnValue($exportedShippingAddress)); + + $paypalInfo->expects($this->once()) + ->method('importToPayment') + ->with($api, $quote->getPayment()); + + $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); + + $checkoutModel->returnFromPaypal('token'); + $this->assertEquals(Onepage::METHOD_GUEST, $quote->getCheckoutMethod()); + } + + /** + * Prepare fixture for exported address. * * @param array $addressData * @return \Magento\Framework\DataObject @@ -216,7 +286,7 @@ protected function _getExportedAddressFixture(array $addressData) protected function _getFixtureQuote() { /** @var \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection */ - $quoteCollection = $this->_objectManager->create('Magento\Quote\Model\ResourceModel\Quote\Collection'); + $quoteCollection = $this->_objectManager->create(\Magento\Quote\Model\ResourceModel\Quote\Collection::class); return $quoteCollection->getLastItem(); } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php new file mode 100644 index 0000000000000..b541f3e7cb9f0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php @@ -0,0 +1,90 @@ +loadArea('adminhtml'); +$objectManager->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'carriers/flatrate/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); +$objectManager->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'payment/paypal_express/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); +$product->load(1); + +$billingData = [ + 'firstname' => 'testname', + 'lastname' => 'lastname', + 'company' => '', + 'email' => 'test@com.com', + 'street' => [ + 0 => 'test1', + 1 => '', + ], + 'city' => 'Test', + 'region_id' => '1', + 'region' => '', + 'postcode' => '9001', + 'country_id' => 'US', + 'telephone' => '11111111', + 'fax' => '', + 'confirm_password' => '', + 'save_in_address_book' => '1', + 'use_for_shipping' => '1', +]; + +$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class, ['data' => $billingData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId( + $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId() + ) + ->setReservedOrderId('100000002') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, 10); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); +$quote->setCustomerEmail('admin@example.com'); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php new file mode 100644 index 0000000000000..897e3f379d9c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php @@ -0,0 +1,77 @@ +loadArea('adminhtml'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); +$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + +/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $customerRepository->getById(1); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 100, + ]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); +$product->load(1); + +$customerBillingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerBillingAddress->load(1); +$billingAddressDataObject = $customerBillingAddress->getDataModel(); +$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$billingAddress->importCustomerAddressData($billingAddressDataObject); +$billingAddress->setAddressType('billing'); + +/** @var \Magento\Customer\Model\Address $customerShippingAddress */ +$customerShippingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerShippingAddress->load(2); +$shippingAddressDataObject = $customerShippingAddress->getDataModel(); +$shippingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$shippingAddress->importCustomerAddressData($shippingAddressDataObject); +$shippingAddress->setAddressType('shipping'); + +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(false) + ->setCustomerId($customer->getId()) + ->setCustomer($customer) + ->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()) + ->setReservedOrderId('test02') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, 10); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php index b868e64a8af0f..2bc22ff98339a 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php @@ -3,98 +3,10 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\App\Config\MutableScopeConfigInterface' -)->setValue( - 'carriers/flatrate/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\App\Config\MutableScopeConfigInterface' -)->setValue( - 'payment/paypal_express/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData( - [ - 'qty' => 100, - 'is_in_stock' => 1, - ] - )->save(); -$product->load(1); -$billingData = [ - 'firstname' => 'testname', - 'lastname' => 'lastname', - 'company' => '', - 'email' => 'test@com.com', - 'street' => [ - 0 => 'test1', - 1 => '', - ], - 'city' => 'Test', - 'region_id' => '1', - 'region' => '', - 'postcode' => '9001', - 'country_id' => 'US', - 'telephone' => '11111111', - 'fax' => '', - 'confirm_password' => '', - 'save_in_address_book' => '1', - 'use_for_shipping' => '1', -]; - -$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Quote\Model\Quote\Address', ['data' => $billingData]); -$billingAddress->setAddressType('billing'); - -$shippingAddress = clone $billingAddress; -$shippingAddress->setId(null)->setAddressType('shipping'); -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create('Magento\Quote\Model\Quote'); -$quote->setCustomerIsGuest( - true -)->setStoreId( - $objectManager->get( - 'Magento\Store\Model\StoreManagerInterface' - )->getStore()->getId() -)->setReservedOrderId( - '100000002' -)->setBillingAddress( - $billingAddress -)->setShippingAddress( - $shippingAddress -)->addProduct( - $product, - 10 -); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); -$quote->setCustomerEmail('admin@example.com'); +require __DIR__ . '/quote_express.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ $service = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('\Magento\Quote\Api\CartManagementInterface'); + ->create(\Magento\Quote\Api\CartManagementInterface::class); $order = $service->submit($quote, ['increment_id' => '100000002']); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php index 2570bfdeb8220..8879c9a863c63 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php @@ -4,78 +4,8 @@ * See COPYING.txt for license details. */ -require __DIR__ . '/../../Customer/_files/customer.php'; -require __DIR__ . '/../../Customer/_files/customer_two_addresses.php'; - -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); - -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -$objectManager->get('Magento\Framework\App\Config\MutableScopeConfigInterface') - ->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); -$objectManager->get('Magento\Framework\App\Config\MutableScopeConfigInterface') - ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - -/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ -$customerRepository = $objectManager->create('Magento\Customer\Api\CustomerRepositoryInterface'); -$customer = $customerRepository->getById(1); - -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setStockData([ - 'use_config_manage_stock' => 1, - 'qty' => 100, - 'is_qty_decimal' => 0, - 'is_in_stock' => 100, -]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); -$product->load(1); - -$customerBillingAddress = $objectManager->create('Magento\Customer\Model\Address'); -$customerBillingAddress->load(1); -$billingAddressDataObject = $customerBillingAddress->getDataModel(); -$billingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); -$billingAddress->importCustomerAddressData($billingAddressDataObject); -$billingAddress->setAddressType('billing'); - -/** @var \Magento\Customer\Model\Address $customerShippingAddress */ -$customerShippingAddress = $objectManager->create('Magento\Customer\Model\Address'); -$customerShippingAddress->load(2); -$shippingAddressDataObject = $customerShippingAddress->getDataModel(); -$shippingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); -$shippingAddress->importCustomerAddressData($shippingAddressDataObject); -$shippingAddress->setAddressType('shipping'); - -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create('Magento\Quote\Model\Quote'); -$quote->setCustomerIsGuest(false) - ->setCustomerId($customer->getId()) - ->setCustomer($customer) - ->setStoreId($objectManager->get('Magento\Store\Model\StoreManagerInterface')->getStore()->getId()) - ->setReservedOrderId('test02') - ->setBillingAddress($billingAddress) - ->setShippingAddress($shippingAddress) - ->addProduct($product, 10); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ -$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); +require __DIR__ . '/quote_express_with_customer.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ -$service = $objectManager->create('\Magento\Quote\Api\CartManagementInterface'); +$service = $objectManager->create(\Magento\Quote\Api\CartManagementInterface::class); $order = $service->submit($quote, ['increment_id' => '100000002']); From 205fb60e2258bc005c1c0fa6f629e7b6c81d9fe2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 12:56:56 +0300 Subject: [PATCH 095/363] MAGETWO-64730: [Backport] - Cannot place order with PayPal Express (last product in stock) - for 2.1 --- .../testsuite/Magento/Paypal/Model/Express/CheckoutTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index 0fe79ad7bd2db..0d908e4d458ca 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -14,7 +14,9 @@ use Magento\Paypal\Model\Api\Nvp; /** - * Class CheckoutTest + * Checkout test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CheckoutTest extends \PHPUnit_Framework_TestCase { From aa325ad5b12d5caaaddab049f2ad24ef0be8796d Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 16:09:29 +0300 Subject: [PATCH 096/363] MAGETWO-67628: [Backport] - 503 error when trying to make tax_class_id attribute Not Searchable - for 2.1 --- .../Adminhtml/Product/Attribute/Validate.php | 14 ++-- .../Product/Attribute/ValidateTest.php | 18 +++-- .../Adminhtml/Product/AttributeTest.php | 71 +++++++++++++++---- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index cb4d370cf51e8..94fad64f5ed96 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -68,7 +68,7 @@ public function execute() $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); $attributeId = $this->getRequest()->getParam('attribute_id'); $attribute = $this->_objectManager->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class )->loadByCode( $this->_entityTypeId, $attributeCode @@ -87,10 +87,10 @@ public function execute() if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ - $attributeSet = $this->_objectManager->create('Magento\Eav\Model\Entity\Attribute\Set'); + $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { - $setName = $this->_objectManager->get('Magento\Framework\Escaper')->escapeHtml($setName); + $setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName); $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $setName)); $layout = $this->layoutFactory->create(); @@ -149,10 +149,16 @@ private function setMessageToResponse($response, $messages) /** * @param DataObject $response * @param array|null $options + * + * @return void */ private function checkUniqueOption(DataObject $response, array $options = null) { - if (is_array($options) && !$this->isUniqueAdminValues($options['value'], $options['delete'])) { + if (is_array($options) + && !empty($options['value']) + && !empty($options['delete']) + && !$this->isUniqueAdminValues($options['value'], $options['delete']) + ) { $this->setMessageToResponse($response, [__('The value of Admin must be unique.')]); $response->setError(true); } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index eb0149544323a..64cab9578c654 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -198,8 +198,16 @@ public function testUniqueValidation(array $options, $isError) public function provideUniqueData() { return [ - // valid options - [ + 'no values' => [ + [ + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ] + ], false + ], + 'valid options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -213,8 +221,7 @@ public function provideUniqueData() ] ], false ], - //with duplicate - [ + 'duplicate options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -228,8 +235,7 @@ public function provideUniqueData() ] ], true ], - //with duplicate but deleted - [ + 'duplicate and deleted' => [ [ 'value' => [ "option_0" => [1, 0], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index ca67bbbaa2769..991b71f6af03a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -28,7 +28,7 @@ public function testWrongFrontendInput() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); $message = $messages->getItemsByType('error')[0]; $this->assertEquals('Input type "some_input" not found in the input types list.', $message->getText()); @@ -53,7 +53,7 @@ public function testWithPopup() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('success')); $message = $messages->getItemsByType('success')[0]; $this->assertEquals('You saved the product attribute.', $message->getText()); @@ -73,7 +73,7 @@ public function testWithExceptionWhenSaveAttribute() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); } @@ -91,7 +91,7 @@ public function testWrongAttributeId() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); /** @var \Magento\Framework\Message\Error $message */ $message = $messages->getItemsByType('error')[0]; @@ -116,7 +116,7 @@ public function testAttributeWithoutId() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('success')); /** @var \Magento\Framework\Message\Success $message */ $message = $messages->getItemsByType('success')[0]; @@ -137,7 +137,7 @@ public function testWrongAttributeCode() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); /** @var \Magento\Framework\Message\Error $message */ $message = $messages->getItemsByType('error')[0]; @@ -171,7 +171,7 @@ public function testSaveActionApplyToDataSystemAttribute() $postData = $this->_getAttributeData() + ['attribute_id' => '2']; $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertNull($model->getData('apply_to')); } @@ -185,7 +185,7 @@ public function testSaveActionApplyToDataUserDefinedAttribute() $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertEquals('simple', $model->getData('apply_to')); } @@ -199,7 +199,7 @@ public function testSaveActionApplyToData() unset($postData['apply_to']); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertEquals(['simple'], $model->getApplyTo()); } @@ -212,7 +212,7 @@ public function testSaveActionApplyToData() public function testSaveActionCleanAttributeLabelCache() { /** @var \Magento\Translation\Model\ResourceModel\StringUtils $string */ - $string = $this->_objectManager->create('Magento\Translation\Model\ResourceModel\StringUtils'); + $string = $this->_objectManager->create(\Magento\Translation\Model\ResourceModel\StringUtils::class); $this->assertEquals('predefined string translation', $this->_translate('string to translate')); $string->saveTranslate('string to translate', 'new string translation'); $postData = $this->_getAttributeData() + ['attribute_id' => 1]; @@ -231,12 +231,12 @@ protected function _translate($string) { // emulate admin store and design \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\View\DesignInterface' + \Magento\Framework\View\DesignInterface::class )->setDesignTheme( 1 ); /** @var \Magento\Framework\TranslateInterface $translate */ - $translate = $this->_objectManager->get('Magento\Framework\TranslateInterface'); + $translate = $this->_objectManager->get(\Magento\Framework\TranslateInterface::class); $translate->loadData(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, true); return __($string); } @@ -270,4 +270,51 @@ protected function _getAttributeData() 'frontend_label' => [\Magento\Store\Model\Store::DEFAULT_STORE_ID => 'string to translate'] ]; } + + /** + * Tests \Magento\Catalog\Controller\Adminhtml\Product\Attribute\Validate. + * + * @dataProvider dataProviderForTestValidate + */ + public function testValidateAttribute($postData) + { + $expectedResult = ['error' => false]; + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); + $model->load($postData['attribute_code'], 'attribute_code'); + $attributeId = $model->getId(); + + $postData['attribute_id'] = $attributeId; + + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product_attribute/validate'); + $response = $this->getResponse()->getBody(); + $this->assertJson($response, 'Validate controller didn\'t return expected result.'); + $result = \Zend_Json::decode($response); + $this->assertEquals($expectedResult, $result, 'Attribute validation didn\'t pass.'); + } + + /** + * Returns data for testValidateAttribute. + * + * @return array + */ + public function dataProviderForTestValidate() + { + return [ + 'tax_class_id' => [ + 'postData' => [ + 'attribute_code' => 'tax_class_id', + 'frontend_label' => 'Tax Class', + 'option' => [ + 'delete' => [ + 0 => '', + 2 => '' + ] + ] + ] + ] + + ]; + } } From c29ae7ba19ef956be7d082744a7927b5ef82271b Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 19 May 2017 18:11:36 +0300 Subject: [PATCH 097/363] MAGETWO-64675: Functional test changes. --- .../Checkout/Test/Block/Onepage/Review.php | 22 +++++++++- .../Test/TestStep/ViewAndEditCartStep.php | 41 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php index 5c667e30ead4d..470feddf3252a 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php @@ -6,12 +6,30 @@ namespace Magento\Checkout\Test\Block\Onepage; -use Magento\Checkout\Test\Block\Onepage\AbstractReview; +use Magento\Mtf\Client\Locator; /** * One page checkout status review block. */ class Review extends AbstractReview { - // + /** + * Review gift card line locator. + * + * @var string + */ + private $giftCardTotalSelector = '//div[contains(@class, "opc-block-summary")]//tr[contains(@class, "giftcard")]'; + + + /** + * Return if gift card is applied. + * + * @return bool + */ + public function isGiftCardApplied() + { + $this->waitForElementNotVisible($this->waitElement); + + return $this->_rootElement->find($this->giftCardTotalSelector, Locator::SELECTOR_XPATH)->isVisible(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php new file mode 100644 index 0000000000000..0d73bed319f7c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php @@ -0,0 +1,41 @@ +checkoutCart = $checkoutCart; + } + + /** + * Proceed to checkout cart page. + * + * @return void + */ + public function run() + { + $this->checkoutCart->open(); + } +} From 4191e0821b953119e963644bf3c90adb832ad78a Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 19 May 2017 18:39:00 +0300 Subject: [PATCH 098/363] MAGETWO-64675: Functional test changes. --- .../tests/app/Magento/Checkout/Test/Block/Onepage/Review.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php index 470feddf3252a..8f47c0f5716ca 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php @@ -20,7 +20,6 @@ class Review extends AbstractReview */ private $giftCardTotalSelector = '//div[contains(@class, "opc-block-summary")]//tr[contains(@class, "giftcard")]'; - /** * Return if gift card is applied. * From dccf439b4448476cf7b50358d5f7014e2e409ce4 Mon Sep 17 00:00:00 2001 From: Andre Flitsch Date: Sat, 20 May 2017 13:43:26 +0200 Subject: [PATCH 099/363] ported fix from 237e54d - MAGETWO-55684: Fix XSD schema Signed-off-by: Andre Flitsch --- .../View/Layout/etc/layout_merged.xsd | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/internal/Magento/Framework/View/Layout/etc/layout_merged.xsd b/lib/internal/Magento/Framework/View/Layout/etc/layout_merged.xsd index 0db909dbc8fd8..665b7504255e0 100644 --- a/lib/internal/Magento/Framework/View/Layout/etc/layout_merged.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/layout_merged.xsd @@ -7,6 +7,8 @@ --> + + @@ -20,29 +22,31 @@ - - + + - - - - - - - - - - - - - - - - - - + + + + + + Layout Type definition + + + + + + + + + + + + + + From b043944085b94d3689b396246753d19a02bf453f Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 22 May 2017 15:57:05 +0300 Subject: [PATCH 100/363] MAGETWO-57475: [Backport] - Layered navigation contains filters for out of stock products - for 2.1 --- .../Product/Indexer/Eav/AbstractEav.php | 16 +- .../Product/Indexer/Eav/Decimal.php | 3 +- .../Product/Indexer/Eav/Source.php | 81 +++-- .../Product/Indexer/Price/DefaultPrice.php | 2 +- .../Magento/Catalog/Setup/InstallSchema.php | 2 +- .../Magento/Catalog/Setup/UpgradeSchema.php | 50 +++ app/code/Magento/Catalog/etc/module.xml | 2 +- .../Mysql/Aggregation/DataProvider.php | 32 +- .../Adapter/Mysql/Filter/AliasResolver.php | 47 +++ .../Adapter/Mysql/Filter/Preprocessor.php | 57 ++- .../Search/FilterMapper/ExclusionStrategy.php | 86 +++++ .../Search/FilterMapper/FilterContext.php | 114 ++++++ .../FilterMapper/FilterStrategyInterface.php | 26 ++ .../FilterMapper/StaticAttributeStrategy.php | 85 +++++ .../FilterMapper/TermDropdownStrategy.php | 149 ++++++++ .../Model/Search/IndexBuilder.php | 7 +- .../Model/Search/TableMapper.php | 198 +++++----- .../Mysql/Filter/AliasResolverTest.php | 70 ++++ .../Adapter/Mysql/Filter/PreprocessorTest.php | 36 +- .../Search/FilterMapper/FilterContextTest.php | 267 ++++++++++++++ .../Unit/Model/Search/TableMapperTest.php | 344 ++++-------------- app/code/Magento/CatalogSearch/etc/di.xml | 1 + .../Repository/CatalogProductAttribute.xml | 25 ++ ...sertProductsInStockInLayeredNavigation.php | 91 +++++ .../ConfigurableAttributesData.xml | 38 ++ .../CreateConfigurableProductEntityTest.xml | 12 + .../Test/Block/Navigation.php | 34 ++ .../InjectableTests/MAGETWO-57475.xml | 15 + .../Search/Adapter/Mysql/AdapterTest.php | 51 ++- .../Search/_files/configurable_attribute.php | 58 +++ .../configurable_attribute_rollback.php | 31 ++ .../Search/_files/product_configurable.php | 145 ++++++++ .../_files/product_configurable_rollback.php | 36 ++ .../Framework/Search/_files/requests.xml | 19 + .../Framework/Search/RequestConfigTest.php | 2 +- .../Search/Adapter/Mysql/Adapter.php | 8 +- .../Mysql/Aggregation/Builder/Metrics.php | 8 +- .../Magento/Framework/Search/etc/requests.xsd | 1 + 38 files changed, 1779 insertions(+), 470 deletions(-) create mode 100644 app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php create mode 100644 app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php create mode 100644 app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php create mode 100644 dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertProductsInStockInLayeredNavigation.php create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index 227b47d572e01..7cd1c8c09fc17 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -197,7 +197,7 @@ protected function _prepareRelationIndexSelect($parentIds = null) )->joinLeft( ['e' => $this->getTable('catalog_product_entity')], 'e.' . $linkField .' = l.parent_id', - ['e.entity_id as parent_id'] + [] )->join( ['cs' => $this->getTable('store')], '', @@ -205,9 +205,17 @@ protected function _prepareRelationIndexSelect($parentIds = null) )->join( ['i' => $idxTable], 'l.child_id = i.entity_id AND cs.store_id = i.store_id', - ['attribute_id', 'store_id', 'value'] + [] )->group( - ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value'] + ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] + )->columns( + [ + 'parent_id' => 'e.entity_id', + 'attribute_id' => 'i.attribute_id', + 'store_id' => 'i.store_id', + 'value' => 'i.value', + 'source_id' => 'l.child_id' + ] ); if ($parentIds !== null) { $select->where('e.entity_id IN(?)', $parentIds); @@ -222,7 +230,7 @@ protected function _prepareRelationIndexSelect($parentIds = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('l.parent_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php index 4a45401630020..76127b02d5fb9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php @@ -85,6 +85,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null) 'pdd.attribute_id', 'cs.store_id', 'value' => $productValueExpression, + 'source_id' => 'cpe.entity_id', ] ); @@ -116,7 +117,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('cpe.entity_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index 74e5147804eb8..e7cf48c378ca0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -178,6 +178,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'pid.attribute_id', 'pid.store_id', 'value' => $ifNullSql, + 'pid.entity_id', ] )->where( 'pid.attribute_id IN(?)', @@ -200,7 +201,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('pid.entity_id'), 'website_field' => new \Zend_Db_Expr('pid.website_id'), - 'store_field' => new \Zend_Db_Expr('pid.store_id') + 'store_field' => new \Zend_Db_Expr('pid.store_id'), ] ); $query = $select->insertFromSelect($idxTable); @@ -221,11 +222,7 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $connection = $this->getConnection(); // prepare multiselect attributes - if ($attributeId === null) { - $attrIds = $this->_getIndexableAttributes(true); - } else { - $attrIds = [$attributeId]; - } + $attrIds = $attributeId === null ? $this->_getIndexableAttributes(true) : [$attributeId]; if (!$attrIds) { return $this; @@ -247,20 +244,20 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value'); $select = $connection->select()->from( ['pvd' => $this->getTable('catalog_product_entity_varchar')], - [$productIdField, 'attribute_id'] + [] )->join( ['cs' => $this->getTable('store')], '', - ['store_id'] + [] )->joinLeft( ['pvs' => $this->getTable('catalog_product_entity_varchar')], "pvs.{$productIdField} = pvd.{$productIdField} AND pvs.attribute_id = pvd.attribute_id" . ' AND pvs.store_id=cs.store_id', - ['value' => $productValueExpression] + [] )->joinLeft( ['cpe' => $this->getTable('catalog_product_entity')], "cpe.{$productIdField} = pvd.{$productIdField}", - ['entity_id'] + [''] )->where( 'pvd.store_id=?', $connection->getIfNullSql('pvs.store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID) @@ -272,6 +269,14 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $attrIds )->where( 'cpe.entity_id IS NOT NULL' + )->columns( + [ + 'entity_id' => 'cpe.entity_id', + 'attribute_id' => 'attribute_id', + 'store_id' => 'cs.store_id', + 'value' => $productValueExpression, + 'source_id' => 'cpe.entity_id', + ] ); $statusCond = $connection->quoteInto('=?', ProductStatus::STATUS_ENABLED); @@ -289,30 +294,11 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu 'select' => $select, 'entity_field' => new \Zend_Db_Expr('cpe.entity_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); - $i = 0; - $data = []; - $query = $select->query(); - while ($row = $query->fetch()) { - $values = explode(',', $row['value']); - foreach ($values as $valueId) { - if (isset($options[$row['attribute_id']][$valueId])) { - $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId]; - $i++; - if ($i % 10000 == 0) { - $this->_saveIndexData($data); - $data = []; - } - } - } - } - - $this->_saveIndexData($data); - unset($options); - unset($data); + $this->saveDataFromSelect($select, $options); return $this; } @@ -331,12 +317,43 @@ protected function _saveIndexData(array $data) $connection = $this->getConnection(); $connection->insertArray( $this->getIdxTable(), - ['entity_id', 'attribute_id', 'store_id', 'value'], + ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id'], $data ); + return $this; } + /** + * Prepares data from select to save. + * + * @param \Magento\Framework\DB\Select $select + * @param array $options + * + * @return void + */ + private function saveDataFromSelect(\Magento\Framework\DB\Select $select, array $options) + { + $i = 0; + $data = []; + $query = $select->query(); + while ($row = $query->fetch()) { + $values = explode(',', $row['value']); + foreach ($values as $valueId) { + if (isset($options[$row['attribute_id']][$valueId])) { + $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId, $row['source_id']]; + $i++; + if ($i % 10000 == 0) { + $this->_saveIndexData($data); + $data = []; + } + } + } + } + + $this->_saveIndexData($data); + } + /** * Retrieve temporary source index table name * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 41456945191fa..fbf2ea6eadaa9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -368,7 +368,7 @@ protected function prepareFinalPriceDataForType($entityIds, $type) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('e.entity_id'), 'website_field' => new \Zend_Db_Expr('cw.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php index 206bd820e59fb..4e164d53e317d 100644 --- a/app/code/Magento/Catalog/Setup/InstallSchema.php +++ b/app/code/Magento/Catalog/Setup/InstallSchema.php @@ -18,6 +18,7 @@ class InstallSchema implements InstallSchemaInterface /** * {@inheritdoc} * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Db_Exception */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { @@ -2426,7 +2427,6 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con 'option_id', $installer->getTable('catalog_product_option'), 'option_id', - \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE, \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE ) ->setComment( diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php index aef2501aa9aaa..213cbe8b0cf7c 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php +++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php @@ -32,9 +32,59 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con if (version_compare($context->getVersion(), '2.0.6', '<')) { $this->addUniqueKeyToCategoryProductTable($setup); } + + if (version_compare($context->getVersion(), '2.1.4', '<')) { + $this->addSourceEntityIdToProductEavIndex($setup); + } + $setup->endSetup(); } + /** + * Add the column 'source_id' to the Product EAV index tables. + * It allows to identify which entity was used to create value in the index. + * It is useful to identify original entity in a composite products. + * + * @param SchemaSetupInterface $setup + * + * @return void + */ + private function addSourceEntityIdToProductEavIndex(SchemaSetupInterface $setup) + { + $tables = [ + 'catalog_product_index_eav', + 'catalog_product_index_eav_idx', + 'catalog_product_index_eav_tmp', + 'catalog_product_index_eav_decimal', + 'catalog_product_index_eav_decimal_idx', + 'catalog_product_index_eav_decimal_tmp', + ]; + $connection = $setup->getConnection(); + + foreach ($tables as $tableName) { + $tableName = $setup->getTable($tableName); + $connection->addColumn( + $tableName, + 'source_id', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + 'unsigned' => true, + 'nullable' => false, + 'default' => 0, + 'comment' => 'Original entity Id for attribute value', + ] + ); + $connection->dropIndex($tableName, $connection->getPrimaryKeyName($tableName)); + $primaryKeyFields = ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id']; + $setup->getConnection()->addIndex( + $tableName, + $connection->getIndexName($tableName, $primaryKeyFields), + $primaryKeyFields, + \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY + ); + } + } + /** * @param SchemaSetupInterface $setup * @return void diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml index ffbd5bb6b206d..571176694547a 100644 --- a/app/code/Magento/Catalog/etc/module.xml +++ b/app/code/Magento/Catalog/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php index d4f16f7f012f3..495b22048b4d5 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -15,6 +15,7 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; use Magento\Framework\Search\Request\BucketInterface; +use Magento\CatalogInventory\Model\Stock; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -79,7 +80,13 @@ public function getDataSet( $select = $this->getSelect(); - if ($attribute->getAttributeCode() == 'price') { + $select->joinInner( + ['entities' => $entityIdsTable->getName()], + 'main_table.entity_id = entities.entity_id', + [] + ); + + if ($attribute->getAttributeCode() === 'price') { /** @var \Magento\Store\Model\Store $store */ $store = $this->scopeResolver->getScope($currentScope); if (!$store instanceof \Magento\Store\Model\Store) { @@ -94,19 +101,24 @@ public function getDataSet( $currentScopeId = $this->scopeResolver->getScope($currentScope) ->getId(); $table = $this->resource->getTableName( - 'catalog_product_index_eav' . ($attribute->getBackendType() == 'decimal' ? '_decimal' : '') + 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '') ); - $select->from(['main_table' => $table], ['value']) + $subSelect = $select; + $subSelect->from(['main_table' => $table], ['main_table.value']) + ->joinLeft( + ['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')], + 'main_table.source_id = stock_index.product_id', + [] + ) ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) - ->where('main_table.store_id = ? ', $currentScopeId); + ->where('main_table.store_id = ? ', $currentScopeId) + ->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK) + ->group(['main_table.entity_id', 'main_table.value']); + $parentSelect = $this->getSelect(); + $parentSelect->from(['main_table' => $subSelect], ['main_table.value']); + $select = $parentSelect; } - $select->joinInner( - ['entities' => $entityIdsTable->getName()], - 'main_table.entity_id = entities.entity_id', - [] - ); - return $select; } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php new file mode 100644 index 0000000000000..52a2c6ff40c99 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -0,0 +1,47 @@ +getField(); + switch ($field) { + case 'price': + $alias = 'price_index'; + break; + case 'category_ids': + $alias = 'category_ids_index'; + break; + default: + $alias = $field . RequestGenerator::FILTER_SUFFIX; + break; + } + + return $alias; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index 63a98b3a6455b..056524452645e 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -18,6 +18,10 @@ use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Store\Model\Store; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\ScopeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -60,10 +64,26 @@ class Preprocessor implements PreprocessorInterface private $metadataPool; /** + * @deprecated + * * @var TableMapper */ private $tableMapper; + /** + * Scope config. + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Resolving table alias for Search Request filter. + * + * @var AliasResolver + */ + private $aliasResolver; + /** * @param ConditionManager $conditionManager * @param ScopeResolverInterface $scopeResolver @@ -71,6 +91,8 @@ class Preprocessor implements PreprocessorInterface * @param ResourceConnection $resource * @param TableMapper $tableMapper * @param string $attributePrefix + * @param ScopeConfigInterface $scopeConfig + * @param AliasResolver $aliasResolver */ public function __construct( ConditionManager $conditionManager, @@ -78,7 +100,9 @@ public function __construct( Config $config, ResourceConnection $resource, TableMapper $tableMapper, - $attributePrefix + $attributePrefix, + ScopeConfigInterface $scopeConfig = null, + AliasResolver $aliasResolver = null ) { $this->conditionManager = $conditionManager; $this->scopeResolver = $scopeResolver; @@ -87,6 +111,8 @@ public function __construct( $this->connection = $resource->getConnection(); $this->attributePrefix = $attributePrefix; $this->tableMapper = $tableMapper; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->aliasResolver = $aliasResolver ?: ObjectManager::getInstance()->get(AliasResolver::class); } /** @@ -117,7 +143,7 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu } elseif ($filter->getField() === 'category_ids') { return 'category_ids_index.category_id = ' . (int) $filter->getValue(); } elseif ($attribute->isStatic()) { - $alias = $this->tableMapper->getMappingAlias($filter); + $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( $this->connection->quoteIdentifier($attribute->getAttributeCode()), $this->connection->quoteIdentifier($alias . '.' . $attribute->getAttributeCode()), @@ -208,7 +234,7 @@ private function processRangeNumeric(FilterInterface $filter, $query, $attribute */ private function processTermSelect(FilterInterface $filter, $isNegation) { - $alias = $this->tableMapper->getMappingAlias($filter); + $alias = $this->aliasResolver->getAlias($filter); if (is_array($filter->getValue())) { $value = sprintf( '%s IN (%s)', @@ -224,9 +250,34 @@ private function processTermSelect(FilterInterface $filter, $isNegation) $value ); + if ($this->isAddStockFilter()) { + $resultQuery = sprintf( + '%1$s AND %2$s%3$s.stock_status = %4$s', + $resultQuery, + $alias, + AliasResolver::STOCK_FILTER_SUFFIX, + Stock::STOCK_IN_STOCK + ); + } + return $resultQuery; } + /** + * Checks if it is necessary to show out of stock products. + * + * @return bool + */ + private function isAddStockFilter() + { + $isShowOutOfStock = $this->scopeConfig->isSetFlag( + 'cataloginventory/options/show_out_of_stock', + ScopeInterface::SCOPE_STORE + ); + + return false === $isShowOutOfStock; + } + /** * Get product metadata pool * diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php new file mode 100644 index 0000000000000..03675f27cac21 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -0,0 +1,86 @@ +resourceConnection = $resourceConnection; + $this->storeManager = $storeManager; + $this->aliasResolver = $aliasResolver; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $isApplied = false; + $field = $filter->getField(); + if ('price' === $field) { + $alias = $this->aliasResolver->getAlias($filter); + $tableName = $this->resourceConnection->getTableName('catalog_product_index_price'); + $select->joinInner( + [$alias => $tableName], + $this->resourceConnection->getConnection()->quoteInto( + 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?', + $this->storeManager->getWebsite()->getId() + ), + [] + ); + $isApplied = true; + } elseif ('category_ids' === $field) { + $alias = $this->aliasResolver->getAlias($filter); + $tableName = $this->resourceConnection->getTableName('catalog_category_product_index'); + $select->joinInner( + [$alias => $tableName], + 'search_index.entity_id = category_ids_index.product_id', + [] + ); + $isApplied = true; + } + + return $isApplied; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php new file mode 100644 index 0000000000000..8f75bbde9af5a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php @@ -0,0 +1,114 @@ +eavConfig = $eavConfig; + $this->aliasResolver = $aliasResolver; + $this->exclusionStrategy = $exclusionStrategy; + $this->termDropdownStrategy = $termDropdownStrategy; + $this->staticAttributeStrategy = $staticAttributeStrategy; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $isApplied = $this->exclusionStrategy->apply($filter, $select); + + if (!$isApplied) { + $attribute = $this->getAttributeByCode($filter->getField()); + + if ($attribute) { + if ($filter->getType() === \Magento\Framework\Search\Request\FilterInterface::TYPE_TERM + && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) + ) { + $isApplied = $this->termDropdownStrategy->apply($filter, $select); + } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) { + $isApplied = $this->staticAttributeStrategy->apply($filter, $select); + } + } + } + + return $isApplied; + } + + /** + * Returns attribute by attribute_code. + * + * @param string $field + * + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php new file mode 100644 index 0000000000000..0e41e0cd69f9a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php @@ -0,0 +1,26 @@ +resourceConnection = $resourceConnection; + $this->eavConfig = $eavConfig; + $this->aliasResolver = $aliasResolver; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $attribute = $this->getAttributeByCode($filter->getField()); + $alias = $this->aliasResolver->getAlias($filter); + $select->joinInner( + [$alias => $attribute->getBackendTable()], + 'search_index.entity_id = ' + . $this->resourceConnection->getConnection()->quoteIdentifier("$alias.entity_id"), + [] + ); + + return true; + } + + /** + * Returns attribute by attribute_code. + * + * @param string $field + * + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php new file mode 100644 index 0000000000000..a0f7f706b4105 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php @@ -0,0 +1,149 @@ +storeManager = $storeManager; + $this->resourceConnection = $resourceConnection; + $this->eavConfig = $eavConfig; + $this->scopeConfig = $scopeConfig; + $this->aliasResolver = $aliasResolver; + } + + /** + * Applies filter. + * + * @param \Magento\Framework\Search\Request\FilterInterface $filter + * @param \Magento\Framework\DB\Select $select + * + * @return bool is filter was applied + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $alias = $this->aliasResolver->getAlias($filter); + $attribute = $this->getAttributeByCode($filter->getField()); + $joinCondition = sprintf( + 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d', + $alias, + $attribute->getId(), + $this->storeManager->getWebsite()->getId() + ); + $select->joinLeft( + [$alias => $this->resourceConnection->getTableName('catalog_product_index_eav')], + $joinCondition, + [] + ); + + if ($this->isAddStockFilter()) { + $stockAlias = $alias . AliasResolver::STOCK_FILTER_SUFFIX; + $select->joinLeft( + [$stockAlias => $this->resourceConnection->getTableName('cataloginventory_stock_status')], + sprintf('%2$s.product_id = %1$s.source_id', $alias, $stockAlias), + [] + ); + } + + return true; + } + + /** + * Returns attribute by attribute code. + * + * @param string $field + * + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } + + /** + * Check if it is necessary to show out of stock products. + * + * @return bool + */ + private function isAddStockFilter() + { + $isShowOutOfStock = $this->scopeConfig->isSetFlag( + 'cataloginventory/options/show_out_of_stock', + ScopeInterface::SCOPE_STORE + ); + + return false === $isShowOutOfStock; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php index 333c44af3424f..03129b0d69859 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php @@ -21,7 +21,7 @@ use Magento\Framework\App\ScopeResolverInterface; /** - * Build base Query for Index + * Build base Query for Index. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class IndexBuilder implements IndexBuilderInterface @@ -99,6 +99,7 @@ public function __construct( * * @param RequestInterface $request * @return Select + * @throws \LogicException */ public function build(RequestInterface $request) { @@ -132,7 +133,7 @@ public function build(RequestInterface $request) ), [] ); - $select->where('stock_index.stock_status = ?', Stock::DEFAULT_STOCK_ID); + $select->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK); } return $select; @@ -147,7 +148,7 @@ private function getStockConfiguration() { if ($this->stockConfiguration === null) { $this->stockConfiguration = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\CatalogInventory\Api\StockConfigurationInterface'); + ->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class); } return $this->stockConfiguration; } diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php index fc6c98b22a1c7..2b8b79127c962 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php @@ -7,7 +7,10 @@ namespace Magento\CatalogSearch\Model\Search; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; -use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; +use Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\DB\Select; use Magento\Framework\Search\Request\FilterInterface; @@ -16,14 +19,22 @@ use Magento\Framework\Search\RequestInterface; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** + * Responsibility of the TableMapper is to collect all filters from the search query + * and pass them one by one for processing in the FilterContext, + * which will apply them to the Select. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.NPathComplexity) */ class TableMapper { /** - * @var Resource + * Resource connection. + * + * @var AppResource */ private $resource; @@ -33,121 +44,128 @@ class TableMapper private $storeManager; /** + * @deprecated + * * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */ private $attributeCollection; /** + * Eav attribute config. + * + * @var EavConfig + */ + private $eavConfig; + + /** + * Scope config. + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * FilterStrategyInterface provides the interface to work with strategies. + * + * @var FilterStrategyInterface + */ + private $filterStrategy; + + /** + * Table alias resolver for Search Request filter. + * + * @var AliasResolver + */ + private $aliasResolver; + + /** + * TableMapper constructor. * @param AppResource $resource * @param StoreManagerInterface $storeManager * @param CollectionFactory $attributeCollectionFactory + * @param EavConfig|null $eavConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param FilterStrategyInterface|null $filterStrategy + * @param AliasResolver|null $aliasResolver */ public function __construct( AppResource $resource, StoreManagerInterface $storeManager, - CollectionFactory $attributeCollectionFactory + CollectionFactory $attributeCollectionFactory, + EavConfig $eavConfig = null, + ScopeConfigInterface $scopeConfig = null, + FilterStrategyInterface $filterStrategy = null, + AliasResolver $aliasResolver = null ) { $this->resource = $resource; $this->storeManager = $storeManager; $this->attributeCollection = $attributeCollectionFactory->create(); + $this->eavConfig = $eavConfig ?: ObjectManager::getInstance()->get(EavConfig::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->filterStrategy = $filterStrategy ?: ObjectManager::getInstance()->get(FilterStrategyInterface::class); + $this->aliasResolver = $aliasResolver ?: ObjectManager::getInstance()->get(AliasResolver::class); } /** + * Adds tables to select. + * * @param Select $select * @param RequestInterface $request + * * @return Select */ public function addTables(Select $select, RequestInterface $request) { - $mappedTables = []; - $filters = $this->getFilters($request->getQuery()); + $appliedFilters = []; + $filters = $this->getFiltersFromQuery($request->getQuery()); + foreach ($filters as $filter) { - list($alias, $table, $mapOn, $mappedFields) = $this->getMappingData($filter); - if (!array_key_exists($alias, $mappedTables)) { - $select->joinLeft( - [$alias => $table], - $mapOn, - $mappedFields - ); - $mappedTables[$alias] = $table; + $alias = $this->aliasResolver->getAlias($filter); + + if (!array_key_exists($alias, $appliedFilters)) { + $isApplied = $this->filterStrategy->apply($filter, $select); + if ($isApplied) { + $appliedFilters[$alias] = true; + } } } + return $select; } /** + * This method is deprecated. + * Please use \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::getAlias() instead. + * + * @deprecated + * @see AliasResolver::getAlias() + * * @param FilterInterface $filter * @return string */ public function getMappingAlias(FilterInterface $filter) { - list($alias) = $this->getMappingData($filter); - return $alias; - } - - /** - * Returns mapping data for field in format: [ - * 'table_alias', - * 'table', - * 'join_condition', - * ['fields'] - * ] - * @param FilterInterface $filter - * @return array - */ - private function getMappingData(FilterInterface $filter) - { - $alias = null; - $table = null; - $mapOn = null; - $mappedFields = null; - $field = $filter->getField(); - $fieldToTableMap = $this->getFieldToTableMap($field); - if ($fieldToTableMap) { - list($alias, $table, $mapOn, $mappedFields) = $fieldToTableMap; - $table = $this->resource->getTableName($table); - } elseif ($attribute = $this->getAttributeByCode($field)) { - if ($filter->getType() === FilterInterface::TYPE_TERM - && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) - ) { - $table = $this->resource->getTableName('catalog_product_index_eav'); - $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, - $attribute->getId(), - $this->getStoreId() - ); - $mappedFields = []; - } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) { - $table = $attribute->getBackendTable(); - $alias = $field . RequestGenerator::FILTER_SUFFIX; - $mapOn = 'search_index.entity_id = ' . $alias . '.entity_id'; - $mappedFields = null; - } - } - - return [$alias, $table, $mapOn, $mappedFields]; + return $this->aliasResolver->getAlias($filter); } /** * @param RequestQueryInterface $query * @return FilterInterface[] */ - private function getFilters($query) + private function getFiltersFromQuery(RequestQueryInterface $query) { $filters = []; switch ($query->getType()) { case RequestQueryInterface::TYPE_BOOL: /** @var \Magento\Framework\Search\Request\Query\BoolExpression $query */ foreach ($query->getMust() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } foreach ($query->getShould() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } foreach ($query->getMustNot() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } break; case RequestQueryInterface::TYPE_FILTER: @@ -196,56 +214,4 @@ private function getFiltersFromBoolFilter(BoolExpression $boolExpression) } return $filters; } - - /** - * @return int - */ - private function getWebsiteId() - { - return $this->storeManager->getWebsite()->getId(); - } - - /** - * @return int - */ - private function getStoreId() - { - return $this->storeManager->getStore()->getId(); - } - - /** - * @param string $field - * @return array|null - */ - private function getFieldToTableMap($field) - { - $fieldToTableMap = [ - 'price' => [ - 'price_index', - 'catalog_product_index_price', - $this->resource->getConnection()->quoteInto( - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?', - $this->getWebsiteId() - ), - [] - ], - 'category_ids' => [ - 'category_ids_index', - 'catalog_category_product_index', - 'search_index.entity_id = category_ids_index.product_id', - [] - ] - ]; - return array_key_exists($field, $fieldToTableMap) ? $fieldToTableMap[$field] : null; - } - - /** - * @param string $field - * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute - */ - private function getAttributeByCode($field) - { - $attribute = $this->attributeCollection->getItemByColumnValue('attribute_code', $field); - return $attribute; - } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php new file mode 100644 index 0000000000000..cb7b12fc698ef --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php @@ -0,0 +1,70 @@ +aliasResolver = $objectManagerHelper->getObject( + \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::class, + [] + ); + } + + /** + * @param string $field + * @param string $expectedAlias + * @dataProvider aliasDataProvider + */ + public function testGetFilterAlias($field, $expectedAlias) + { + $filter = $this->getMockBuilder(\Magento\Framework\Search\Request\Filter\Term::class) + ->setMethods(['getField']) + ->disableOriginalConstructor() + ->getMock(); + $filter->expects($this->once()) + ->method('getField') + ->willReturn($field); + + $this->assertSame($expectedAlias, $this->aliasResolver->getAlias($filter)); + } + + /** + * @return array + */ + public function aliasDataProvider() + { + return [ + 'general' => [ + 'field' => 'general', + 'alias' => 'general' . RequestGenerator::FILTER_SUFFIX, + ], + 'price' => [ + 'field' => 'price', + 'alias' => 'price_index', + ], + 'category_ids' => [ + 'field' => 'category_ids', + 'alias' => 'category_ids_index', + ], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index 357e6d3669b62..c8dd43c74a10d 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -11,6 +11,7 @@ use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -77,42 +78,47 @@ class PreprocessorTest extends \PHPUnit_Framework_TestCase */ private $metadataPoolMock; + /** + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $aliasResolver; + protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); - $this->conditionManager = $this->getMockBuilder('\Magento\Framework\Search\Adapter\Mysql\ConditionManager') + $this->conditionManager = $this->getMockBuilder(\Magento\Framework\Search\Adapter\Mysql\ConditionManager::class) ->disableOriginalConstructor() ->setMethods(['wrapBrackets']) ->getMock(); - $this->scopeResolver = $this->getMockBuilder('\Magento\Framework\App\ScopeResolverInterface') + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) ->disableOriginalConstructor() ->setMethods(['getScope']) ->getMockForAbstractClass(); - $this->scope = $this->getMockBuilder('\Magento\Framework\App\ScopeInterface') + $this->scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class) ->disableOriginalConstructor() ->setMethods(['getId']) ->getMockForAbstractClass(); $this->scopeResolver->expects($this->any()) ->method('getScope') ->will($this->returnValue($this->scope)); - $this->config = $this->getMockBuilder('\Magento\Eav\Model\Config') + $this->config = $this->getMockBuilder(\Magento\Eav\Model\Config::class) ->disableOriginalConstructor() ->setMethods(['getAttribute']) ->getMock(); - $this->attribute = $this->getMockBuilder('\Magento\Eav\Model\Entity\Attribute\AbstractAttribute') + $this->attribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() ->setMethods(['getBackendTable', 'isStatic', 'getAttributeId', 'getAttributeCode', 'getFrontendInput']) ->getMockForAbstractClass(); - $this->resource = $resource = $this->getMockBuilder('\Magento\Framework\App\ResourceConnection') + $this->resource = $resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->setMethods(['getConnection', 'getTableName']) ->getMock(); - $this->connection = $this->getMockBuilder('\Magento\Framework\DB\Adapter\AdapterInterface') + $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->setMethods(['select', 'getIfNullSql', 'quote']) ->getMockForAbstractClass(); - $this->select = $this->getMockBuilder('\Magento\Framework\DB\Select') + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->setMethods(['from', 'join', 'where', '__toString', 'joinLeft', 'columns', 'having']) ->getMock(); @@ -125,7 +131,7 @@ protected function setUp() $resource->expects($this->atLeastOnce()) ->method('getConnection') ->will($this->returnValue($this->connection)); - $this->filter = $this->getMockBuilder('\Magento\Framework\Search\Request\FilterInterface') + $this->filter = $this->getMockBuilder(\Magento\Framework\Search\Request\FilterInterface::class) ->disableOriginalConstructor() ->setMethods(['getField', 'getValue', 'getType']) ->getMockForAbstractClass(); @@ -141,7 +147,10 @@ function ($select) { ) ); - $this->tableMapper = $this->getMockBuilder('\Magento\CatalogSearch\Model\Search\TableMapper') + $this->tableMapper = $this->getMockBuilder(\Magento\CatalogSearch\Model\Search\TableMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->aliasResolver = $this->getMockBuilder(AliasResolver::class) ->disableOriginalConstructor() ->getMock(); $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) @@ -156,7 +165,7 @@ function ($select) { $metadata->expects($this->any())->method('getLinkField')->willReturn('entity_id'); $this->target = $objectManagerHelper->getObject( - 'Magento\CatalogSearch\Model\Adapter\Mysql\Filter\Preprocessor', + \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\Preprocessor::class, [ 'conditionManager' => $this->conditionManager, 'scopeResolver' => $this->scopeResolver, @@ -165,6 +174,7 @@ function ($select) { 'attributePrefix' => 'attr_', 'metadataPool' => $this->metadataPoolMock, 'tableMapper' => $this->tableMapper, + 'aliasResolver' => $this->aliasResolver, ] ); } @@ -234,7 +244,7 @@ public function testProcessStaticAttribute() $this->attribute->method('getAttributeCode') ->willReturn('static_attribute'); - $this->tableMapper->expects($this->once())->method('getMappingAlias') + $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('attr_table_alias'); $this->filter->expects($this->exactly(3)) ->method('getField') @@ -272,7 +282,7 @@ public function testProcessTermFilter($frontendInput, $fieldValue, $isNegation, ->method('getFrontendInput') ->willReturn($frontendInput); - $this->tableMapper->expects($this->once())->method('getMappingAlias') + $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('termAttrAlias'); $this->filter->expects($this->exactly(3)) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php new file mode 100644 index 0000000000000..119855a131475 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php @@ -0,0 +1,267 @@ +eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + $this->aliasResolver = $this->getMockBuilder( + AliasResolver::class + ) + ->disableOriginalConstructor() + ->setMethods(['getAlias']) + ->getMock(); + $this->exclusionStrategy = $this->getMockBuilder(ExclusionStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->termDropdownStrategy = $this->getMockBuilder(TermDropdownStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->staticAttributeStrategy = $this->getMockBuilder(StaticAttributeStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $objectManager = new ObjectManager($this); + $this->filterContext = $objectManager->getObject( + FilterContext::class, + [ + 'eavConfig' => $this->eavConfig, + 'aliasResolver' => $this->aliasResolver, + 'exclusionStrategy' => $this->exclusionStrategy, + 'termDropdownStrategy' => $this->termDropdownStrategy, + 'staticAttributeStrategy' => $this->staticAttributeStrategy, + ] + ); + } + + /** + * Tests apply() method on exclusion filter. + */ + public function testApplyOnExclusionFilter() + { + $filter = $this->createFilterMock(); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + $this->eavConfig->expects($this->never())->method('getAttribute'); + + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + /** + * Tests apply() method without attribute. + */ + public function testApplyFilterWithoutAttribute() + { + $filter = $this->createFilterMock('some_field'); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'some_field') + ->willReturn(null); + + $this->assertFalse($this->filterContext->apply($filter, $this->select)); + } + + /** + * Tests apply() method on term filter by select attribute. + */ + public function testApplyOnTermFilterBySelect() + { + $filter = $this->createFilterMock('select_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('select'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'select_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->termDropdownStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + /** + * Tests apply() method on term filter by multiselect attribute. + */ + public function testApplyOnTermFilterByMultiSelect() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('multiselect'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->termDropdownStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + /** + * Tests apply() method on term filter by static attribute. + */ + public function testApplyOnTermFilterByStaticAttribute() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('text', AbstractAttribute::TYPE_STATIC); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->staticAttributeStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + /** + * Tests apply() method on term filter by unknown attribute type. + */ + public function testApplyOnTermFilterByUnknownAttributeType() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('text', 'text'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + + $this->assertFalse($this->filterContext->apply($filter, $this->select)); + } + + /** + * Creates filter mock. + * + * @param string $field + * @param string $type + * + * @return FilterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function createFilterMock($field = null, $type = null) + { + $filter = $this->getMockBuilder(FilterInterface::class) + ->setMethods(['getField', 'getType']) + ->getMockForAbstractClass(); + $filter->expects($this->any()) + ->method('getField') + ->willReturn($field); + $filter->expects($this->any()) + ->method('getType') + ->willReturn($type); + + return $filter; + } + + /** + * Creaates attribute mock. + * + * @param string|null $frontendInput + * @param string|null $backendType + * + * @return Attribute|\PHPUnit_Framework_MockObject_MockObject + */ + private function createAttributeMock($frontendInput = null, $backendType = null) + { + $attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getFrontendInput', 'getBackendType']) + ->getMock(); + $attribute->expects($this->any()) + ->method('getFrontendInput') + ->willReturn($frontendInput); + $attribute->expects($this->any()) + ->method('getBackendType') + ->willReturn($backendType); + + return $attribute; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php index f0200c115a7a0..19cca14d38046 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php @@ -8,7 +8,10 @@ use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; -use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; /** * Test for \Magento\CatalogSearch\Model\Search\TableMapper @@ -16,9 +19,6 @@ */ class TableMapperTest extends \PHPUnit_Framework_TestCase { - const WEBSITE_ID = 4512; - const STORE_ID = 2514; - /** * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection|\PHPUnit_Framework_MockObject_MockObject */ @@ -55,91 +55,86 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase private $resource; /** - * @var \Magento\Store\Api\Data\StoreInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogSearch\Model\Search\TableMapper */ - private $store; + private $target; /** - * @var \Magento\CatalogSearch\Model\Search\TableMapper + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject */ - private $target; + private $aliasResolver; + + /** + * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; protected function setUp() { $objectManager = new ObjectManager($this); - $this->connection = $this->getMockBuilder('\Magento\Framework\DB\Adapter\AdapterInterface') + $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->connection->expects($this->any()) - ->method('quoteInto') - ->willReturnCallback( - function ($query, $expression) { - return str_replace('?', $expression, $query); - } - ); - - $this->resource = $this->getMockBuilder('\Magento\Framework\App\ResourceConnection') + $this->connection->expects($this->never())->method('quoteInto'); + + $this->resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->resource->method('getTableName') - ->willReturnCallback( - function ($table) { - return 'prefix_' . $table; - } - ); - $this->resource->expects($this->any()) - ->method('getConnection') - ->willReturn($this->connection); - - $this->website = $this->getMockBuilder('\Magento\Store\Api\Data\WebsiteInterface') - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->website->expects($this->any()) - ->method('getId') - ->willReturn(self::WEBSITE_ID); - $this->store = $this->getMockBuilder('\Magento\Store\Api\Data\StoreInterface') + + $this->resource->expects($this->never())->method('getTableName'); + $this->resource->expects($this->never())->method('getConnection'); + + $this->website = $this->getMockBuilder(\Magento\Store\Api\Data\WebsiteInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->store->expects($this->any()) - ->method('getId') - ->willReturn(self::STORE_ID); - $this->storeManager = $this->getMockBuilder('\Magento\Store\Model\StoreManagerInterface') + $this->website->expects($this->never())->method('getId'); + + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getWebsite') - ->willReturn($this->website); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - $this->attributeCollection = $this->getMockBuilder( - '\Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection' - ) + $this->storeManager->expects($this->never())->method('getWebsite'); + $this->storeManager->expects($this->never())->method('getStore'); + + $this->attributeCollection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $attributeCollectionFactory = $this->getMockBuilder( - '\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory' - ) + $attributeCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $attributeCollectionFactory->expects($this->once()) ->method('create') ->willReturn($this->attributeCollection); + + $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->setMethods(['getAttribute']) + ->disableOriginalConstructor() + ->getMock(); + + $this->aliasResolver = $this->getMockBuilder(AliasResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->aliasResolver->expects($this->any()) + ->method('getAlias') + ->willReturnCallback(function (FilterInterface $filter) { + return $filter->getField() . '_alias'; + }); $this->target = $objectManager->getObject( - '\Magento\CatalogSearch\Model\Search\TableMapper', + \Magento\CatalogSearch\Model\Search\TableMapper::class, [ 'resource' => $this->resource, 'storeManager' => $this->storeManager, - 'attributeCollectionFactory' => $attributeCollectionFactory + 'attributeCollectionFactory' => $attributeCollectionFactory, + 'eavConfig' => $this->eavConfig, + 'aliasResolver' => $this->aliasResolver, ] ); - $this->select = $this->getMockBuilder('\Magento\Framework\DB\Select') + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->request = $this->getMockBuilder('\Magento\Framework\Search\RequestInterface') + $this->request = $this->getMockBuilder(\Magento\Framework\Search\RequestInterface::class) ->disableOriginalConstructor() ->getMock(); } @@ -151,14 +146,7 @@ public function testAddPriceFilter() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->with( - ['price_index' => 'prefix_catalog_product_index_price'], - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID, - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } @@ -167,18 +155,11 @@ public function testAddStaticAttributeFilter() { $priceFilter = $this->createRangeFilter('static'); $query = $this->createFilterQuery($priceFilter); - $this->createAttributeMock('static', 'static', 'backend_table', 0, 'select'); + $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->with( - ['static_filter' => 'backend_table'], - 'search_index.entity_id = static_filter.entity_id', - null - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } @@ -190,46 +171,25 @@ public function testAddCategoryIds() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->with( - ['category_ids_index' => 'prefix_catalog_category_product_index'], - 'search_index.entity_id = category_ids_index.product_id', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddTermFilter() { - $this->createAttributeMock('color', null, null, 132, 'select', 0); $categoryIdsFilter = $this->createTermFilter('color'); $query = $this->createFilterQuery($categoryIdsFilter); $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->with( - ['color_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = color_filter.entity_id' - . ' AND color_filter.attribute_id = 132' - . ' AND color_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolQueryWithTermFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); - $query = $this->createBoolQuery( [ $this->createFilterQuery($this->createTermFilter('must1')), @@ -244,45 +204,13 @@ public function testAddBoolQueryWithTermFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolQueryWithTermAndPriceFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createBoolQuery( [ $this->createFilterQuery($this->createTermFilter('must1')), @@ -298,53 +226,13 @@ public function testAddBoolQueryWithTermAndPriceFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['price_index' => 'prefix_catalog_product_index_price'], - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID, - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(3)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolFilterWithTermFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createFilterQuery( $this->createBoolFilter( [ @@ -361,45 +249,13 @@ public function testAddBoolFilterWithTermFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolFilterWithBoolFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createFilterQuery( $this->createBoolFilter( [ @@ -416,47 +272,19 @@ public function testAddBoolFilterWithBoolFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } /** * @param $filter + * * @return \Magento\Framework\Search\Request\Query\Filter|\PHPUnit_Framework_MockObject_MockObject */ private function createFilterQuery($filter) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Query\Filter') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\Filter::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -470,12 +298,14 @@ private function createFilterQuery($filter) * @param array $must * @param array $should * @param array $mustNot + * * @return \Magento\Framework\Search\Request\Query\BoolExpression|\PHPUnit_Framework_MockObject_MockObject + * * @internal param $filter */ private function createBoolQuery(array $must, array $should, array $mustNot) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Query\BoolExpression') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\BoolExpression::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -493,12 +323,14 @@ private function createBoolQuery(array $must, array $should, array $mustNot) * @param array $must * @param array $should * @param array $mustNot + * * @return \Magento\Framework\Search\Request\Filter\BoolExpression|\PHPUnit_Framework_MockObject_MockObject + * * @internal param $filter */ private function createBoolFilter(array $must, array $should, array $mustNot) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Filter\BoolExpression') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Filter\BoolExpression::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -514,12 +346,13 @@ private function createBoolFilter(array $must, array $should, array $mustNot) /** * @param string $field + * * @return \Magento\Framework\Search\Request\Filter\Range|\PHPUnit_Framework_MockObject_MockObject */ private function createRangeFilter($field) { $filter = $this->createFilterMock( - '\Magento\Framework\Search\Request\Filter\Range', + \Magento\Framework\Search\Request\Filter\Range::class, FilterInterface::TYPE_RANGE, $field ); @@ -528,12 +361,13 @@ private function createRangeFilter($field) /** * @param string $field + * * @return \Magento\Framework\Search\Request\Filter\Term|\PHPUnit_Framework_MockObject_MockObject */ private function createTermFilter($field) { $filter = $this->createFilterMock( - '\Magento\Framework\Search\Request\Filter\Term', + \Magento\Framework\Search\Request\Filter\Term::class, FilterInterface::TYPE_TERM, $field ); @@ -544,6 +378,7 @@ private function createTermFilter($field) * @param string $class * @param string $type * @param string $field + * * @return \PHPUnit_Framework_MockObject_MockObject */ private function createFilterMock($class, $type, $field) @@ -555,40 +390,7 @@ private function createFilterMock($class, $type, $field) ->willReturn($type); $filter->method('getField') ->willReturn($field); - return $filter; - } - /** - * @param string $code - * @param string $backendType - * @param string $backendTable - * @param int $attributeId - * @param string $frontendInput - * @param int $positionInCollection - */ - private function createAttributeMock( - $code, - $backendType = null, - $backendTable = null, - $attributeId = 120, - $frontendInput = 'select', - $positionInCollection = 0 - ) { - $attribute = $this->getMockBuilder('\Magento\Catalog\Model\ResourceModel\Eav\Attribute') - ->setMethods(['getBackendType', 'getBackendTable', 'getId', 'getFrontendInput']) - ->disableOriginalConstructor() - ->getMock(); - $attribute->method('getId') - ->willReturn($attributeId); - $attribute->method('getBackendType') - ->willReturn($backendType); - $attribute->method('getBackendTable') - ->willReturn($backendTable); - $attribute->method('getFrontendInput') - ->willReturn($frontendInput); - $this->attributeCollection->expects($this->at($positionInCollection)) - ->method('getItemByColumnValue') - ->with('attribute_code', $code) - ->willReturn($attribute); + return $filter; } } diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index fcea9f1563898..3e5e949aa8b45 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -11,6 +11,7 @@ + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductAttribute.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductAttribute.xml index ce161091f2804..c289394e02aee 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductAttribute.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductAttribute.xml @@ -204,5 +204,30 @@ + + + attribute_dropdown%isolation% + attribute_dropdown%isolation% + Dropdown + No + Filterable (with results) + + + No + black + black_%isolation% + + + No + white + white_%isolation% + + + No + yellow + yellow_%isolation% + + + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertProductsInStockInLayeredNavigation.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertProductsInStockInLayeredNavigation.php new file mode 100644 index 0000000000000..2760447acf501 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertProductsInStockInLayeredNavigation.php @@ -0,0 +1,91 @@ +getDataFieldConfig('category_ids')['source']->getCategories()[0]; + + // create simple product with the same category as configurable. + $productSimple = $fixtureFactory->createByCode( + 'catalogProductSimple', + [ + 'dataset' => 'default', + 'data' => [ + 'category_ids' => [ + 'category' => $sourceCategory, + ], + ] + ] + ); + $productSimple->persist(); + + $categoryName = $product->getCategoryIds()[0]; + + // open category on frontend. + $cmsIndex->open(); + $cmsIndex->getTopmenu()->selectCategoryByName($categoryName); + + // get configurable attribute name. + $attributeName = ''; + foreach ($product->getConfigurableAttributesData()['attributes_data'] as $data) { + $attributeName = $data['label']; + } + + // get options which are visible. + $visibleOptions = + $catalogCategoryView->getLayeredNavigationBlock()->getOptionsContentForAttribute($attributeName); + + $expectedVisibleOptions = []; + $attributesData = $product->getConfigurableAttributesData()['attributes_data']; + + // get options which shouldd be visible. + foreach ($product->getConfigurableAttributesData()['matrix'] as $key => $value) { + if ($value['qty'] > 0) { + $attribute = explode(':', $key)[0]; + $option = explode(':', $key)[1]; + $expectedVisibleOptions[] = $attributesData[$attribute]['options'][$option]['label']; + } + } + + \PHPUnit_Framework_Assert::assertEquals($expectedVisibleOptions, $visibleOptions); + } + + /** + * Only products in stock is presented in layered navigation. + * + * @return string + */ + public function toString() + { + return 'Only products in stock is presented in layered navigation'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml index 09899f446a76d..0b6818c381708 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml @@ -694,5 +694,43 @@ + + + + + + + 12.00 + Yes + + + 10.00 + Yes + + + 35.00 + Yes + + + + + + catalogProductAttribute::filterable_dropdown_three_options_without_default + + + + 100 + 1 + + + 0 + 1 + + + 33 + 22 + + + diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml index 86cd87ffac1bf..157f4417e7924 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml @@ -180,5 +180,17 @@ + + with_one_out_of_stock_item + Configurable Product %isolation% + configurable_sku_%isolation% + 100 + default + no + default_anchor_subcategory + configurable-product-%isolation% + catalogProductSimple::default + + diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php index 01ca5a8020410..9c3619bda1eec 100644 --- a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php @@ -49,6 +49,15 @@ class Navigation extends Block */ protected $expandFilterButton = '[data]'; + // @codingStandardsIgnoreStart + /** + * Locator value for correspondent Attribute filter option contents. + * + * @var string + */ + protected $optionContent = './/*[@id="narrow-by-list"]/div[contains(@class,"filter-options-item") and contains(@class,"active")]//li[@class="item"]/a'; + // @codingStandardsIgnoreEnd + /** * Remove all applied filters. * @@ -104,4 +113,29 @@ public function applyFilter($filter, $linkPattern) } throw new \Exception("Can't find {$filter} filter link by pattern: {$linkPattern}"); } + + /** + * Gets all available filters with options. + * + * @param $attributeName + * @return array + */ + public function getOptionsContentForAttribute($attributeName) + { + $this->waitForElementVisible($this->loadedNarrowByList); + + $this->_rootElement->find( + sprintf($this->optionTitle, $attributeName), + Locator::SELECTOR_XPATH + )->click(); + + $options = $this->_rootElement->getElements($this->optionContent, Locator::SELECTOR_XPATH); + $data = []; + + foreach ($options as $option) { + $data[] = explode(' ', $option->getText())[0]; + } + + return $data; + } } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml new file mode 100644 index 0000000000000..0f93be3f78636 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file 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 d0c9eb50474fe..f5e7ff999ce88 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 @@ -43,18 +43,18 @@ protected function setUp() $this->objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Framework\Search\Request\Config\Converter $converter */ - $converter = $this->objectManager->create('Magento\Framework\Search\Request\Config\Converter'); + $converter = $this->objectManager->create(\Magento\Framework\Search\Request\Config\Converter::class); $document = new \DOMDocument(); $document->load($this->getRequestConfigPath()); $requestConfig = $converter->convert($document); /** @var \Magento\Framework\Search\Request\Config $config */ - $config = $this->objectManager->create('Magento\Framework\Search\Request\Config'); + $config = $this->objectManager->create(\Magento\Framework\Search\Request\Config::class); $config->merge($requestConfig); $this->requestBuilder = $this->objectManager->create( - 'Magento\Framework\Search\Request\Builder', + \Magento\Framework\Search\Request\Builder::class, ['config' => $config] ); @@ -76,7 +76,7 @@ protected function getRequestConfigPath() */ protected function assertPreConditions() { - $currentEngine = $this->objectManager->get('Magento\Framework\App\Config\MutableScopeConfigInterface') + $currentEngine = $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) ->getValue(EngineInterface::CONFIG_ENGINE_PATH, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); $this->assertEquals($this->searchEngine, $currentEngine); } @@ -86,7 +86,7 @@ protected function assertPreConditions() */ protected function createAdapter() { - return $this->objectManager->create('Magento\Framework\Search\Adapter\Mysql\Adapter'); + return $this->objectManager->create(\Magento\Framework\Search\Adapter\Mysql\Adapter::class); } /** @@ -370,17 +370,17 @@ public function advancedSearchDataProvider() public function testCustomFilterableAttribute() { /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ - $attribute = $this->objectManager->get('Magento\Catalog\Model\ResourceModel\Eav\Attribute') + $attribute = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'select_attribute'); /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $selectOptions */ $selectOptions = $this->objectManager - ->create('Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection') + ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class) ->setAttributeFilter($attribute->getId()); $attribute->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_attribute'); /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $multiselectOptions */ $multiselectOptions = $this->objectManager - ->create('Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection') + ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class) ->setAttributeFilter($attribute->getId()); $this->requestBuilder->bind('select_attribute', $selectOptions->getLastItem()->getId()); @@ -413,6 +413,41 @@ public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount) $this->assertEquals($expectedRecordsCount, $queryResponse->count()); } + /** + * Tests configurable product search with out of stock option. + * + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php + * @magentoConfigFixture current_store catalog/search/engine mysql + */ + public function testAdvancedSearchConfigProductWithOutOfStockOption() + { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + $attribute = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) + ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable'); + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $selectOptions */ + $selectOptions = $this->objectManager + ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class) + ->setAttributeFilter($attribute->getId()); + + $firstOption = $selectOptions->getFirstItem(); + $firstOptionId = $firstOption->getId(); + $this->requestBuilder->bind('test_configurable', $firstOptionId); + $this->requestBuilder->setRequestName('filter_out_of_stock_child'); + + $queryResponse = $this->executeQuery(); + + $this->assertEquals(0, $queryResponse->count()); + + $secondOption = $selectOptions->getLastItem(); + $secondOptionId = $secondOption->getId(); + $this->requestBuilder->bind('test_configurable', $secondOptionId); + $this->requestBuilder->setRequestName('filter_out_of_stock_child'); + + $queryResponse = $this->executeQuery(); + + $this->assertEquals(1, $queryResponse->count()); + } + public function dateDataProvider() { return [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php new file mode 100644 index 0000000000000..a1dc2589f2891 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php @@ -0,0 +1,58 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute); +} + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php new file mode 100644 index 0000000000000..0029589ae981b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php @@ -0,0 +1,31 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} + +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php new file mode 100644 index 0000000000000..f7909cdd3f5e6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -0,0 +1,145 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +$isFirstOption = true; +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => (int)!$isFirstOption, + ] + ); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock((int)!$isFirstOption); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $isFirstOption = false; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productToDelete = $productRepository->getById(1); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { +// Nothing to remove +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php new file mode 100644 index 0000000000000..25d58cdaccdde --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php @@ -0,0 +1,36 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); 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 148b875261ab0..835189f56f4b4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml @@ -387,4 +387,23 @@ 0 10 + + + + + + + + + + + + + + + + + 0 + 10 + diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php index ce6a21a32546a..85a8f524d5e23 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php @@ -100,7 +100,7 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null) Element 'filterReference': The attribute 'ref' is required but missing. Element 'filter': The attribute 'field' is required but missing. Element 'metric', attribute 'type': [facet 'enumeration'] " . - "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max'}. + "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max', 'avg'}. Element 'metric', attribute 'type': 'sumasdasd' is not a valid value of the local atomic type. Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges ). Element 'request': Missing child element(s). Expected is ( from )." diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php index 48fedcc859a7f..b348d5a454bb8 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php @@ -70,7 +70,13 @@ public function __construct( } /** - * {@inheritdoc} + * Process Search Request. + * + * @param RequestInterface $request + * + * @return \Magento\Framework\Search\Response\QueryResponse + * + * @throws \LogicException */ public function query(RequestInterface $request) { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php index 212ff64b0f492..239bb22ee7755 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php @@ -10,14 +10,14 @@ class Metrics { /** - * Available metrics + * Available metrics. * * @var string[] */ - private $mapMetrics = ['count', 'sum', 'min', 'max', 'avg']; + private $allowedMetrics = ['count', 'sum', 'min', 'max', 'avg']; /** - * Build metrics for Select->columns + * Build metrics for Select->columns. * * @param RequestBucketInterface $bucket * @return string[] @@ -30,7 +30,7 @@ public function build(RequestBucketInterface $bucket) foreach ($metrics as $metric) { $metricType = $metric->getType(); - if (in_array($metricType, $this->mapMetrics)) { + if (in_array($metricType, $this->allowedMetrics, true)) { $selectAggregations[$metricType] = "$metricType(main_table.value)"; } } diff --git a/lib/internal/Magento/Framework/Search/etc/requests.xsd b/lib/internal/Magento/Framework/Search/etc/requests.xsd index 4ad146b0e7be4..67ce202c7f951 100644 --- a/lib/internal/Magento/Framework/Search/etc/requests.xsd +++ b/lib/internal/Magento/Framework/Search/etc/requests.xsd @@ -263,6 +263,7 @@ + From 8198b366eaffcfef137d985b5153192bf2245df5 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 22 May 2017 16:43:41 +0300 Subject: [PATCH 101/363] MAGETWO-57475: [Backport] - Layered navigation contains filters for out of stock products - for 2.1 --- .../Model/Search/FilterMapper/FilterContext.php | 12 ++++++------ .../Search/FilterMapper/StaticAttributeStrategy.php | 2 +- .../Framework/Search/Adapter/Mysql/AdapterTest.php | 1 + .../Framework/Search/_files/product_configurable.php | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php index 8f75bbde9af5a..e1901afce4ffd 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php @@ -35,13 +35,13 @@ class FilterContext implements FilterStrategyInterface * * @var TermDropdownStrategy */ - private $termDropdownStrategy; + private $termDropdownStrategy; - /** - * Strategy for handling static attributes. - * - * @var StaticAttributeStrategy - */ + /** + * Strategy for handling static attributes. + * + * @var StaticAttributeStrategy + */ private $staticAttributeStrategy; /** diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php index 57681e7c97279..a930219d2e24b 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php @@ -62,7 +62,7 @@ public function apply( $select->joinInner( [$alias => $attribute->getBackendTable()], 'search_index.entity_id = ' - . $this->resourceConnection->getConnection()->quoteIdentifier("$alias.entity_id"), + . $this->resourceConnection->getConnection()->quoteIdentifier("$alias.entity_id"), [] ); 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 f5e7ff999ce88..0ea897c8dcf76 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 @@ -12,6 +12,7 @@ /** * Class AdapterTest * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @magentoDataFixture Magento/Framework/Search/_files/products.php diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php index f7909cdd3f5e6..48a2603736822 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -126,7 +126,7 @@ 'product_id = ' . $productToDelete->getId() ); } catch (\Exception $e) { -// Nothing to remove + // Nothing to remove } $registry->unregister('isSecureArea'); From e85342f445260ff27f0c9a81aacfbe0a5aea95a2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 23 May 2017 14:25:52 +0300 Subject: [PATCH 102/363] MAGETWO-57475: [Backport] - Layered navigation contains filters for out of stock products - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-57475.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml deleted file mode 100644 index 0f93be3f78636..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-57475.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file From af98f595c010453430bc7d129f25ad7da788352a Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Tue, 23 May 2017 22:17:59 +0300 Subject: [PATCH 103/363] MAGETWO-67535: [Backport] - When duplicating a product wrong images are assigned to the new duplicate product - for 2.1 --- .../Magento/Catalog/Model/Product/Copier.php | 16 ++++++-- .../Model/Product/Gallery/CreateHandler.php | 4 +- .../Test/Unit/Model/Product/CopierTest.php | 37 ++++++++++++------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index 7095cc038ed39..0af477af9b9d7 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -9,6 +9,9 @@ use Magento\Catalog\Api\Data\ProductInterface; +/** + * Catalog product copier. + */ class Copier { /** @@ -54,12 +57,15 @@ public function copy(\Magento\Catalog\Model\Product $product) $product->getWebsiteIds(); $product->getCategoryIds(); + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + /** @var \Magento\Catalog\Model\Product $duplicate */ $duplicate = $this->productFactory->create(); $duplicate->setData($product->getData()); $duplicate->setOptions([]); $duplicate->setIsDuplicate(true); - $duplicate->setOriginalId($product->getEntityId()); + $duplicate->setOriginalLinkId($product->getData($metadata->getLinkField())); $duplicate->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); $duplicate->setCreatedAt(null); $duplicate->setUpdatedAt(null); @@ -81,11 +87,11 @@ public function copy(\Magento\Catalog\Model\Product $product) } } while (!$isDuplicateSaved); $this->getOptionRepository()->duplicate($product, $duplicate); - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); $product->getResource()->duplicate( $product->getData($metadata->getLinkField()), $duplicate->getData($metadata->getLinkField()) ); + return $duplicate; } @@ -97,8 +103,9 @@ private function getOptionRepository() { if (null === $this->optionRepository) { $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\Product\Option\Repository'); + ->get(\Magento\Catalog\Model\Product\Option\Repository::class); } + return $this->optionRepository; } @@ -110,8 +117,9 @@ private function getMetadataPool() { if (null === $this->metadataPool) { $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\MetadataPool'); + ->get(\Magento\Framework\EntityManager\MetadataPool::class); } + return $this->metadataPool; } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index f95865eb5f33b..f2c0d82e4b73f 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -278,6 +278,8 @@ protected function processNewImage($product, array &$image) } /** + * Duplicate product media gallery data. + * * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -294,7 +296,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], - $product->getOriginalId(), + $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php index 211eae79e439e..a3e2663fc41d2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php @@ -7,6 +7,9 @@ use \Magento\Catalog\Model\Product\Copier; +/** + * Test class for \Magento\Catalog\Model\Product\Copier. + */ class CopierTest extends \PHPUnit_Framework_TestCase { /** @@ -20,17 +23,17 @@ class CopierTest extends \PHPUnit_Framework_TestCase protected $_model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product\CopyConstructorInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $copyConstructorMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $productFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ protected $productMock; @@ -41,29 +44,29 @@ class CopierTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->copyConstructorMock = $this->getMock('\Magento\Catalog\Model\Product\CopyConstructorInterface'); + $this->copyConstructorMock = $this->getMock(\Magento\Catalog\Model\Product\CopyConstructorInterface::class); $this->productFactoryMock = $this->getMock( - '\Magento\Catalog\Model\ProductFactory', + \Magento\Catalog\Model\ProductFactory::class, ['create'], [], '', false ); $this->optionRepositoryMock = $this->getMock( - 'Magento\Catalog\Model\Product\Option\Repository', + \Magento\Catalog\Model\Product\Option\Repository::class, [], [], '', false ); $this->optionRepositoryMock; - $this->productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $this->productMock = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); $this->productMock->expects($this->any())->method('getEntityId')->willReturn(1); - $this->metadata = $this->getMockBuilder('Magento\Framework\EntityManager\EntityMetadata') + $this->metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) ->disableOriginalConstructor() ->getMock(); - $metadataPool = $this->getMockBuilder('Magento\Framework\EntityManager\MetadataPool') + $metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() ->getMock(); $metadataPool->expects($this->any())->method('getMetadata')->willReturn($this->metadata); @@ -78,6 +81,11 @@ protected function setUp() ]); } + /** + * @covers \Magento\Catalog\Model\Product\Copier::copy + * + * @return void + */ public function testCopy() { $this->productMock->expects($this->atLeastOnce())->method('getWebsiteIds'); @@ -87,18 +95,18 @@ public function testCopy() ['linkField', null, '1'], ]); - $resourceMock = $this->getMock('\Magento\Catalog\Model\ResourceModel\Product', [], [], '', false); + $resourceMock = $this->getMock(\Magento\Catalog\Model\ResourceModel\Product::class, [], [], '', false); $this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock)); $duplicateMock = $this->getMock( - '\Magento\Catalog\Model\Product', + \Magento\Catalog\Model\Product::class, [ '__wakeup', 'setData', 'setOptions', 'getData', 'setIsDuplicate', - 'setOriginalId', + 'setOriginalLinkId', 'setStatus', 'setCreatedAt', 'setUpdatedAt', @@ -117,7 +125,7 @@ public function testCopy() $duplicateMock->expects($this->once())->method('setOptions')->with([]); $duplicateMock->expects($this->once())->method('setIsDuplicate')->with(true); - $duplicateMock->expects($this->once())->method('setOriginalId')->with(1); + $duplicateMock->expects($this->once())->method('setOriginalLinkId')->with(1); $duplicateMock->expects( $this->once() )->method( @@ -154,8 +162,11 @@ public function testCopy() } /** + * Set object non-public properties. + * * @param $object * @param array $properties + * @return void */ private function setProperties($object, $properties = []) { From ae27cdf8c73b88d211e1c513bd567e574bc1d023 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Wed, 24 May 2017 09:34:39 +0300 Subject: [PATCH 104/363] MAGETWO-61266: [Backport] - When attempting to place a reorder after a product is disabled, product still gets added to the cart - for 2.1 --- .../Model/Quote/Item/QuantityValidator.php | 54 ++- .../Initializer/QuantityValidatorTest.php | 453 ++++++++++++++++++ app/code/Magento/Quote/Model/Quote.php | 9 +- .../Quote/Test/Unit/Model/QuoteTest.php | 144 +++--- .../Test/Repository/CatalogProductSimple.xml | 60 +++ .../Sales/Test/Block/Order/History.php | 23 + ...ertReorderButtonIsNotVisibleOnFrontend.php | 82 ++++ .../Test/TestCase/CreateOrderBackendTest.xml | 18 +- .../Magento/Catalog/_files/products.php | 10 +- .../Catalog/_files/products_rollback.php | 15 +- ...uote_with_product_and_payment_rollback.php | 7 + .../Magento/Customer/_files/quote.php | 49 +- .../Customer/_files/quote_rollback.php | 41 ++ .../Magento/Quote/Model/QuoteTest.php | 135 ++++-- .../Quote/_files/is_not_salable_product.php | 40 ++ .../is_not_salable_product_rollback.php | 26 + .../testsuite/Magento/Sales/_files/quote.php | 29 +- .../Magento/Sales/_files/quote_rollback.php | 17 +- 18 files changed, 1038 insertions(+), 174 deletions(-) create mode 100644 app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertReorderButtonIsNotVisibleOnFrontend.php create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_product_and_payment_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/quote_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product.php create mode 100644 dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product_rollback.php diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 8d25488dee51c..58cf3ff72c8ec 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -7,9 +7,11 @@ */ namespace Magento\CatalogInventory\Model\Quote\Item; -use Magento\CatalogInventory\Api\StockRegistryInterface; -use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\CatalogInventory\Model\Stock; +/** + * Quantity validation. + */ class QuantityValidator { /** @@ -23,26 +25,30 @@ class QuantityValidator protected $stockItemInitializer; /** - * @var StockRegistryInterface + * Stock registry. + * + * @var \Magento\CatalogInventory\Api\StockRegistryInterface */ protected $stockRegistry; /** - * @var StockStateInterface + * Stock state. + * + * @var \Magento\CatalogInventory\Api\StockStateInterface */ protected $stockState; /** * @param QuantityValidator\Initializer\Option $optionInitializer * @param QuantityValidator\Initializer\StockItem $stockItemInitializer - * @param StockRegistryInterface $stockRegistry - * @param StockStateInterface $stockState + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockStateInterface $stockState */ public function __construct( QuantityValidator\Initializer\Option $optionInitializer, QuantityValidator\Initializer\StockItem $stockItemInitializer, - StockRegistryInterface $stockRegistry, - StockStateInterface $stockState + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockStateInterface $stockState ) { $this->optionInitializer = $optionInitializer; $this->stockItemInitializer = $stockItemInitializer; @@ -65,7 +71,6 @@ public function validate(\Magento\Framework\Event\Observer $observer) { /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ $quoteItem = $observer->getEvent()->getItem(); - if (!$quoteItem || !$quoteItem->getProductId() || !$quoteItem->getQuote() || @@ -73,7 +78,7 @@ public function validate(\Magento\Framework\Event\Observer $observer) ) { return; } - + $product = $quoteItem->getProduct(); $qty = $quoteItem->getQty(); /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ @@ -81,26 +86,32 @@ public function validate(\Magento\Framework\Event\Observer $observer) $quoteItem->getProduct()->getId(), $quoteItem->getProduct()->getStore()->getWebsiteId() ); - /* @var $stockItem \Magento\CatalogInventory\Api\Data\StockItemInterface */ + if (!$stockItem instanceof \Magento\CatalogInventory\Api\Data\StockItemInterface) { throw new \Magento\Framework\Exception\LocalizedException(__('The stock item for Product is not valid.')); } - $parentStockItem = false; + /** @var \Magento\CatalogInventory\Api\Data\StockStatusInterface $stockStatus */ + $stockStatus = $this->stockRegistry->getStockStatus($product->getId(), $product->getStore()->getWebsiteId()); + + /** @var \Magento\CatalogInventory\Api\Data\StockStatusInterface|bool $parentStockStatus */ + $parentStockStatus = false; /** * Check if product in stock. For composite products check base (parent) item stock status */ if ($quoteItem->getParentItem()) { $product = $quoteItem->getParentItem()->getProduct(); - $parentStockItem = $this->stockRegistry->getStockItem( + $parentStockStatus = $this->stockRegistry->getStockStatus( $product->getId(), $product->getStore()->getWebsiteId() ); } - if ($stockItem) { - if (!$stockItem->getIsInStock() || $parentStockItem && !$parentStockItem->getIsInStock()) { + if ($stockStatus) { + if ($stockStatus->getStockStatus() == Stock::STOCK_OUT_OF_STOCK + || $parentStockStatus && $parentStockStatus->getStockStatus() == Stock::STOCK_OUT_OF_STOCK + ) { $quoteItem->addErrorInfo( 'cataloginventory', \Magento\CatalogInventory\Helper\Data::ERROR_QTY, @@ -123,13 +134,13 @@ public function validate(\Magento\Framework\Event\Observer $observer) * Check item for options */ if (($options = $quoteItem->getQtyOptions()) && $qty > 0) { - $qty = $quoteItem->getProduct()->getTypeInstance()->prepareQuoteItemQty($qty, $quoteItem->getProduct()); + $qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product); $quoteItem->setData('qty', $qty); - if ($stockItem) { + if ($stockStatus) { $result = $this->stockState->checkQtyIncrements( - $quoteItem->getProduct()->getId(), + $product->getId(), $qty, - $quoteItem->getProduct()->getStore()->getWebsiteId() + $product->getStore()->getWebsiteId() ); if ($result->getHasError()) { $quoteItem->addErrorInfo( @@ -172,7 +183,10 @@ public function validate(\Magento\Framework\Event\Observer $observer) ); } else { // Delete error from item and its quote, if it was set due to qty lack - $this->_removeErrorsFromQuoteAndItem($quoteItem, \Magento\CatalogInventory\Helper\Data::ERROR_QTY); + $this->_removeErrorsFromQuoteAndItem( + $quoteItem, + \Magento\CatalogInventory\Helper\Data::ERROR_QTY + ); } } } else { diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php new file mode 100644 index 0000000000000..a21036eceeab3 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php @@ -0,0 +1,453 @@ +stockRegistryMock = $this->getMock( + StockRegistry::class, + [], + [], + '', + false + ); + + $this->stockStatusMock = $this->getMock(Status::class, [], [], '', false); + + $this->optionInitializer = $this->getMock(Option::class, [], [], '', false); + $this->stockItemInitializer = $this->getMock(StockItem::class, [], [], '', false); + $this->stockState = $this->getMock(StockState::class, [], [], '', false); + $this->quantityValidator = $objectManagerHelper->getObject( + QuantityValidator::class, + [ + 'optionInitializer' => $this->optionInitializer, + 'stockItemInitializer' => $this->stockItemInitializer, + 'stockRegistry' => $this->stockRegistryMock, + 'stockState' => $this->stockState + ] + ); + $this->observerMock = $this->getMock(Observer::class, [], [], '', false); + $this->eventMock = $this->getMock(Event::class, ['getItem'], [], '', false); + $this->quoteMock = $this->getMock(Quote::class, [], [], '', false); + $this->storeMock = $this->getMock(Store::class, [], [], '', false); + $this->quoteItemMock = $this->getMock( + Item::class, + ['getProductId', 'getQuote', 'getQty', 'getProduct', 'getParentItem', + 'addErrorInfo', 'setData', 'getQtyOptions'], + [], + '', + false + ); + $this->parentItemMock = $this->getMock( + Item::class, + ['getProduct', 'getId', 'getStore'], + [], + '', + false + ); + $this->productMock = $this->getMock(Product::class, [], [], '', false); + $this->stockItemMock = $this->getMock(StockMock::class, [], [], '', false); + $this->parentStockItemMock = $this->getMock(StockMock::class, ['getStockStatus'], [], '', false); + + $this->typeInstanceMock = $this->getMock(Type::class, [], [], '', false); + + $this->resultMock = $this->getMock( + DataObject::class, + ['checkQtyIncrements', 'getMessage', 'getQuoteMessage', 'getHasError'], + [], + '', + false + ); + } + + /** + * This tests the scenario when item is not in stock + * + * @return void + */ + public function testValidateOutOfStock() + { + $this->createInitialStub(0); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + + $this->stockRegistryMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + + $this->quoteItemMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'cataloginventory', + Data::ERROR_QTY, + __('This product is out of stock.') + ); + $this->quoteMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'stock', + 'cataloginventory', + Data::ERROR_QTY, + __('Some of the products are out of stock.') + ); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario when item is in stock but parent is not in stock + * + * @return void + */ + public function testValidateInStock() + { + $this->createInitialStub(1); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + + $this->quoteItemMock->expects($this->any()) + ->method('getParentItem') + ->willReturn($this->parentItemMock); + + $this->stockRegistryMock->expects($this->at(2)) + ->method('getStockStatus') + ->willReturn($this->parentStockItemMock); + + $this->parentStockItemMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(false); + + $this->stockStatusMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturn(true); + + $this->quoteItemMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'cataloginventory', + Data::ERROR_QTY, + __('This product is out of stock.') + ); + $this->quoteMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'stock', + 'cataloginventory', + Data::ERROR_QTY, + __('Some of the products are out of stock.') + ); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario when item is in stock and has options + * + * @return void + */ + public function testValidateWithOptions() + { + $optionMock = $this->getMockBuilder(OptionItem::class) + ->disableOriginalConstructor() + ->setMethods(['setHasError']) + ->getMock(); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + $options = [$optionMock]; + $this->createInitialStub(1); + $this->setUpStubForQuantity(1, true); + $this->setUpStubForRemoveError(); + $this->parentStockItemMock->expects($this->any()) + ->method('getStockStatus') + ->willReturn(true); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(true); + $this->quoteItemMock->expects($this->any()) + ->method('getQtyOptions') + ->willReturn($options); + $this->optionInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + $optionMock->expects($this->never()) + ->method('setHasError'); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario with options but has errors + * + * @return void + */ + public function testValidateWithOptionsAndError() + { + $optionMock = $this->getMockBuilder(OptionItem::class) + ->disableOriginalConstructor() + ->setMethods(['setHasError']) + ->getMock(); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + $options = [$optionMock]; + $this->createInitialStub(1); + $this->setUpStubForQuantity(1, true); + $this->setUpStubForRemoveError(); + $this->parentStockItemMock->expects($this->any()) + ->method('getStockStatus') + ->willReturn(true); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(true); + $this->quoteItemMock->expects($this->any()) + ->method('getQtyOptions') + ->willReturn($options); + $this->optionInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + $optionMock->expects($this->never()) + ->method('setHasError'); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * @param int $qty + * @param bool $hasError + * @return void + */ + private function setUpStubForQuantity($qty, $hasError) + { + $this->productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($this->typeInstanceMock); + $this->typeInstanceMock->expects($this->any()) + ->method('prepareQuoteItemQty') + ->willReturn($qty); + $this->quoteItemMock->expects($this->any()) + ->method('setData'); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->stockState->expects($this->any()) + ->method('checkQtyIncrements') + ->willReturn($this->resultMock); + $this->resultMock->expects($this->any()) + ->method('getHasError') + ->willReturn($hasError); + $this->resultMock->expects($this->any()) + ->method('getMessage') + ->willReturn(''); + $this->resultMock->expects($this->any()) + ->method('getQuoteMessage') + ->willReturn(''); + $this->resultMock->expects($this->any()) + ->method('getQuoteMessageIndex') + ->willReturn(''); + } + + /** + * @param int $qty + * @return void + */ + private function createInitialStub($qty) + { + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(1); + $this->quoteMock->expects($this->any()) + ->method('getIsSuperMode') + ->willReturn(0); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->productMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->quoteItemMock->expects($this->any()) + ->method('getProductId') + ->willReturn(1); + $this->quoteItemMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteItemMock->expects($this->once()) + ->method('getQty') + ->willReturn($qty); + $this->quoteItemMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->eventMock->expects($this->any()) + ->method('getItem') + ->willReturn($this->quoteItemMock); + $this->observerMock->expects($this->any()) + ->method('getEvent') + ->willReturn($this->eventMock); + $this->parentItemMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->parentStockItemMock->expects($this->any()) + ->method('getIsInStock') + ->willReturn(false); + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(1); + $this->quoteItemMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteItemMock->expects($this->any()) + ->method('addErrorInfo'); + $this->quoteMock->expects($this->any()) + ->method('addErrorInfo'); + $this->setUpStubForQuantity(0, false); + $this->stockItemInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + } + + /** + * @return void + */ + private function setUpStubForRemoveError() + { + $quoteItems = [$this->quoteItemMock]; + $this->quoteItemMock->expects($this->any()) + ->method('getHasError') + ->willReturn(false); + $this->quoteMock->expects($this->any()) + ->method('getItemsCollection') + ->willReturn($quoteItems); + $this->quoteMock->expects($this->any()) + ->method('getHasError') + ->willReturn(false); + } +} diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index a7fd735fdc673..4264faa6e5de4 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -487,7 +487,7 @@ public function __construct( */ protected function _construct() { - $this->_init('Magento\Quote\Model\ResourceModel\Quote'); + $this->_init(\Magento\Quote\Model\ResourceModel\Quote::class); } /** @@ -962,7 +962,7 @@ public function setCustomer(\Magento\Customer\Api\Data\CustomerInterface $custom $this->extensibleDataObjectConverter->toFlatArray( $customer, [], - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ) ); $customer->setAddresses($origAddresses); @@ -1570,6 +1570,11 @@ public function addProduct( __('We found an invalid request for adding product to quote.') ); } + if (!$product->isSalable()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Product that you are trying to add is not available.') + ); + } $cartCandidates = $product->getTypeInstance()->prepareForCartAdvanced($request, $product, $processMode); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index 5869540f8dca9..2b32ac639ef36 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -148,14 +148,14 @@ class QuoteTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->quoteAddressFactoryMock = $this->getMock( - 'Magento\Quote\Model\Quote\AddressFactory', + \Magento\Quote\Model\Quote\AddressFactory::class, ['create'], [], '', false ); $this->quoteAddressMock = $this->getMock( - 'Magento\Quote\Model\Quote\Address', + \Magento\Quote\Model\Quote\Address::class, [ 'isDeleted', 'getCollection', 'getId', 'getCustomerAddressId', '__wakeup', 'getAddressType', 'getDeleteImmediately', 'validateMinimumAmount', 'setData' @@ -165,21 +165,21 @@ protected function setUp() false ); $this->quoteAddressCollectionMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Address\Collection', + \Magento\Quote\Model\ResourceModel\Quote\Address\Collection::class, [], [], '', false ); $this->extensibleDataObjectConverterMock = $this->getMock( - 'Magento\Framework\Api\ExtensibleDataObjectConverter', + \Magento\Framework\Api\ExtensibleDataObjectConverter::class, ['toFlatArray'], [], '', false ); $this->customerRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\CustomerRepositoryInterface', + \Magento\Customer\Api\CustomerRepositoryInterface::class, [], '', false, @@ -188,14 +188,14 @@ protected function setUp() ['getById', 'save'] ); $this->objectCopyServiceMock = $this->getMock( - 'Magento\Framework\DataObject\Copy', + \Magento\Framework\DataObject\Copy::class, ['copyFieldsetToTarget'], [], '', false ); - $this->productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); - $this->objectFactoryMock = $this->getMock('\Magento\Framework\DataObject\Factory', ['create'], [], '', false); + $this->productMock = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $this->objectFactoryMock = $this->getMock(\Magento\Framework\DataObject\Factory::class, ['create'], [], '', false); $this->quoteAddressFactoryMock->expects( $this->any() )->method( @@ -210,76 +210,76 @@ protected function setUp() )->will( $this->returnValue($this->quoteAddressCollectionMock) ); - $this->eventManagerMock = $this->getMockBuilder('Magento\Framework\Event\Manager') + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\Manager::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManagerMock = $this->getMockBuilder('Magento\Store\Model\StoreManager') + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) ->disableOriginalConstructor() ->getMock(); - $this->resourceMock = $this->getMockBuilder('Magento\Quote\Model\ResourceModel\Quote') + $this->resourceMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote::class) ->disableOriginalConstructor() ->getMock(); - $this->contextMock = $this->getMockBuilder('Magento\Framework\Model\Context') + $this->contextMock = $this->getMockBuilder(\Magento\Framework\Model\Context::class) ->disableOriginalConstructor() ->getMock(); - $this->customerFactoryMock = $this->getMockBuilder('Magento\Customer\Model\CustomerFactory') + $this->customerFactoryMock = $this->getMockBuilder(\Magento\Customer\Model\CustomerFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->groupRepositoryMock = $this->getMockBuilder('Magento\Customer\Api\GroupRepositoryInterface') + $this->groupRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\GroupRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); $this->contextMock->expects($this->any()) ->method('getEventDispatcher') ->will($this->returnValue($this->eventManagerMock)); $this->quoteItemCollectionFactoryMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory', + \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class, ['create'], [], '', false ); $this->quotePaymentCollectionFactoryMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Payment\CollectionFactory', + \Magento\Quote\Model\ResourceModel\Quote\Payment\CollectionFactory::class, ['create'], [], '', false ); $this->paymentFactoryMock = $this->getMock( - 'Magento\Quote\Model\Quote\PaymentFactory', + \Magento\Quote\Model\Quote\PaymentFactory::class, ['create'], [], '', false ); - $this->scopeConfig = $this->getMockBuilder('Magento\Framework\App\Config') + $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config::class) ->disableOriginalConstructor() ->getMock(); - $this->addressRepositoryMock = $this->getMockForAbstractClass('Magento\Customer\Api\AddressRepositoryInterface', + $this->addressRepositoryMock = $this->getMockForAbstractClass(\Magento\Customer\Api\AddressRepositoryInterface::class, [], '', false ); - $this->criteriaBuilderMock = $this->getMockBuilder('Magento\Framework\Api\SearchCriteriaBuilder') + $this->criteriaBuilderMock = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteriaBuilder::class) ->disableOriginalConstructor() ->getMock(); - $this->filterBuilderMock = $this->getMockBuilder('Magento\Framework\Api\FilterBuilder') + $this->filterBuilderMock = $this->getMockBuilder(\Magento\Framework\Api\FilterBuilder::class) ->disableOriginalConstructor() ->getMock(); $this->extensionAttributesJoinProcessorMock = $this->getMock( - 'Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface', + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class, [], [], '', false ); $this->customerDataFactoryMock = $this->getMock( - 'Magento\Customer\Api\Data\CustomerInterfaceFactory', + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create'], [], '', @@ -287,7 +287,7 @@ protected function setUp() ); $this->quote = (new ObjectManager($this)) ->getObject( - 'Magento\Quote\Model\Quote', + \Magento\Quote\Model\Quote::class, [ 'quoteAddressFactory' => $this->quoteAddressFactoryMock, 'storeManager' => $this->storeManagerMock, @@ -386,7 +386,7 @@ public function dataProviderForTestIsMultipleShippingAddresses() protected function getAddressMock($type) { $shippingAddressMock = $this->getMock( - 'Magento\Quote\Model\Quote\Address', + \Magento\Quote\Model\Quote\Address::class, ['getAddressType', '__wakeup'], [], '', @@ -400,7 +400,7 @@ protected function getAddressMock($type) public function testGetStoreIdNoId() { - $storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); $storeMock->expects($this->once()) @@ -426,7 +426,7 @@ public function testGetStore() { $storeId = 1; - $storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); $this->storeManagerMock->expects($this->once()) @@ -436,14 +436,14 @@ public function testGetStore() $this->quote->setStoreId($storeId); $result = $this->quote->getStore(); - $this->assertInstanceOf('Magento\Store\Model\Store', $result); + $this->assertInstanceOf(\Magento\Store\Model\Store::class, $result); } public function testSetStore() { $storeId = 1; - $storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); $storeMock->expects($this->once()) @@ -451,7 +451,7 @@ public function testSetStore() ->will($this->returnValue($storeId)); $result = $this->quote->setStore($storeMock); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); } public function testGetSharedWebsiteStoreIds() @@ -459,7 +459,7 @@ public function testGetSharedWebsiteStoreIds() $sharedIds = null; $storeIds = [1, 2, 3]; - $websiteMock = $this->getMockBuilder('Magento\Store\Model\Website') + $websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class) ->disableOriginalConstructor() ->getMock(); $websiteMock->expects($this->once()) @@ -478,14 +478,14 @@ public function testGetSharedStoreIds() $storeIds = [1, 2, 3]; $storeId = 1; - $websiteMock = $this->getMockBuilder('Magento\Store\Model\Website') + $websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class) ->disableOriginalConstructor() ->getMock(); $websiteMock->expects($this->once()) ->method('getStoreIds') ->will($this->returnValue($storeIds)); - $storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); $storeMock->expects($this->once()) @@ -515,7 +515,7 @@ public function testLoadActive() ->method('dispatch'); $result = $this->quote->loadActive($quoteId); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); } public function testloadByIdWithoutStore() @@ -530,7 +530,7 @@ public function testloadByIdWithoutStore() ->method('dispatch'); $result = $this->quote->loadByIdWithoutStore($quoteId); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); } /** @@ -540,7 +540,7 @@ public function testSetCustomerAddressData() { $customerId = 1; $addressMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\AddressInterface', + \Magento\Customer\Api\Data\AddressInterface::class, [], '', false, @@ -554,15 +554,15 @@ public function testSetCustomerAddressData() $addresses = [$addressMock]; - $customerMock = $this->getMockForAbstractClass('Magento\Customer\Api\Data\CustomerInterface', [], '', false); + $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class, [], '', false); $customerResultMock = $this->getMockForAbstractClass( - 'Magento\Customer\Api\Data\CustomerInterface', + \Magento\Customer\Api\Data\CustomerInterface::class, [], '', false ); $requestMock = $this->getMock( - '\Magento\Framework\DataObject' + \Magento\Framework\DataObject::class ); $this->extensibleDataObjectConverterMock->expects($this->any()) @@ -586,7 +586,7 @@ public function testSetCustomerAddressData() ->with($this->equalTo(['customer_id' => $customerId])) ->will($this->returnValue($requestMock)); $result = $this->quote->setCustomerAddressData([$addressMock]); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); $this->assertEquals($customerResultMock, $this->quote->getCustomer()); } @@ -594,7 +594,7 @@ public function testGetCustomerTaxClassId() { $groupId = 1; $taxClassId = 1; - $groupMock = $this->getMockForAbstractClass('Magento\Customer\Api\Data\GroupInterface', [], '', false); + $groupMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\GroupInterface::class, [], '', false); $groupMock->expects($this->once()) ->method('getTaxClassId') ->willReturn($taxClassId); @@ -774,7 +774,7 @@ public function testRemoveAddress() $this->quote->setId($id); $result = $this->quote->removeAddress($id); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); } public function testRemoveAllAddresses() @@ -817,7 +817,7 @@ public function testRemoveAllAddresses() $this->quote->setId($id); $result = $this->quote->removeAllAddresses(); - $this->assertInstanceOf('Magento\Quote\Model\Quote', $result); + $this->assertInstanceOf(\Magento\Quote\Model\Quote::class, $result); } /** @@ -832,15 +832,19 @@ public function testAddProductNoCandidates() { $expectedResult = 'test_string'; $requestMock = $this->getMock( - '\Magento\Framework\DataObject' + \Magento\Framework\DataObject::class ); $this->objectFactoryMock->expects($this->once()) ->method('create') ->with($this->equalTo(['qty' => 1])) ->will($this->returnValue($requestMock)); - + + $this->productMock->expects($this->once()) + ->method('isSalable') + ->willReturn(true); + $typeInstanceMock = $this->getMock( - 'Magento\Catalog\Model\Product\Type\Simple', + \Magento\Catalog\Model\Product\Type\Simple::class, [ 'prepareForCartAdvanced' ], @@ -862,7 +866,7 @@ public function testAddProductNoCandidates() public function testAddProductItemPreparation() { $itemMock = $this->getMock( - '\Magento\Quote\Model\Quote\Item', + \Magento\Quote\Model\Quote\Item::class, [], [], '', @@ -871,7 +875,7 @@ public function testAddProductItemPreparation() $expectedResult = $itemMock; $requestMock = $this->getMock( - '\Magento\Framework\DataObject' + \Magento\Framework\DataObject::class ); $this->objectFactoryMock->expects($this->once()) ->method('create') @@ -879,7 +883,7 @@ public function testAddProductItemPreparation() ->will($this->returnValue($requestMock)); $typeInstanceMock = $this->getMock( - 'Magento\Catalog\Model\Product\Type\Simple', + \Magento\Catalog\Model\Product\Type\Simple::class, [ 'prepareForCartAdvanced' ], @@ -889,7 +893,7 @@ public function testAddProductItemPreparation() ); $productMock = $this->getMock( - 'Magento\Catalog\Model\Product', + \Magento\Catalog\Model\Product::class, [ 'getParentProductId', 'setStickWithinParent', @@ -901,7 +905,7 @@ public function testAddProductItemPreparation() ); $collectionMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Item\Collection', + \Magento\Quote\Model\ResourceModel\Quote\Item\Collection::class, [], [], '', @@ -920,6 +924,10 @@ public function testAddProductItemPreparation() $this->quoteItemCollectionFactoryMock->expects($this->once()) ->method('create') ->will($this->returnValue($collectionMock)); + + $this->productMock->expects($this->once()) + ->method('isSalable') + ->willReturn(true); $typeInstanceMock->expects($this->once()) ->method('prepareForCartAdvanced') @@ -988,7 +996,7 @@ public function testGetPaymentIsNotDeleted() { $this->quote->setId(1); $payment = $this->getMock( - 'Magento\Quote\Model\Quote\Payment', + \Magento\Quote\Model\Quote\Payment::class, ['setQuote', 'isDeleted', '__wakeup'], [], '', @@ -1000,7 +1008,7 @@ public function testGetPaymentIsNotDeleted() ->method('isDeleted') ->willReturn(false); $quotePaymentCollectionMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Payment\Collection', + \Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, ['setQuoteFilter', 'getFirstItem'], [], '', @@ -1017,14 +1025,14 @@ public function testGetPaymentIsNotDeleted() ->method('create') ->willReturn($quotePaymentCollectionMock); - $this->assertInstanceOf('\Magento\Quote\Model\Quote\Payment', $this->quote->getPayment()); + $this->assertInstanceOf(\Magento\Quote\Model\Quote\Payment::class, $this->quote->getPayment()); } public function testGetPaymentIsDeleted() { $this->quote->setId(1); $payment = $this->getMock( - 'Magento\Quote\Model\Quote\Payment', + \Magento\Quote\Model\Quote\Payment::class, ['setQuote', 'isDeleted', 'getId', '__wakeup'], [], '', @@ -1039,7 +1047,7 @@ public function testGetPaymentIsDeleted() ->method('getId') ->willReturn(1); $quotePaymentCollectionMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Payment\Collection', + \Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, ['setQuoteFilter', 'getFirstItem'], [], '', @@ -1060,19 +1068,19 @@ public function testGetPaymentIsDeleted() ->method('create') ->willReturn($payment); - $this->assertInstanceOf('\Magento\Quote\Model\Quote\Payment', $this->quote->getPayment()); + $this->assertInstanceOf(\Magento\Quote\Model\Quote\Payment::class, $this->quote->getPayment()); } public function testAddItem() { - $item = $this->getMock('Magento\Quote\Model\Quote\Item', ['setQuote', 'getId'], [], '', false); + $item = $this->getMock(\Magento\Quote\Model\Quote\Item::class, ['setQuote', 'getId'], [], '', false); $item->expects($this->once()) ->method('setQuote'); $item->expects($this->once()) ->method('getId') ->willReturn(false); $itemsMock = $this->getMock( - 'Magento\Eav\Model\Entity\Collection\AbstractCollection', + \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, ['setQuote', 'addItem'], [], '', @@ -1100,7 +1108,7 @@ public function testAddItem() public function testBeforeSaveIsVirtualQuote(array $productTypes, $expected) { $storeId = 1; - $currencyMock = $this->getMockBuilder('Magento\Directory\Model\Currency') + $currencyMock = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) ->disableOriginalConstructor() ->getMock(); $currencyMock->expects($this->any()) @@ -1109,7 +1117,7 @@ public function testBeforeSaveIsVirtualQuote(array $productTypes, $expected) $currencyMock->expects($this->any()) ->method('getRate') ->will($this->returnValue('test_rate')); - $storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); $storeMock->expects($this->once()) @@ -1126,7 +1134,7 @@ public function testBeforeSaveIsVirtualQuote(array $productTypes, $expected) $this->quote->setStoreId($storeId); $collectionMock = $this->getMock( - 'Magento\Quote\Model\ResourceModel\Quote\Item\Collection', + \Magento\Quote\Model\ResourceModel\Quote\Item\Collection::class, [], [], '', @@ -1134,11 +1142,11 @@ public function testBeforeSaveIsVirtualQuote(array $productTypes, $expected) ); $items = []; foreach ($productTypes as $type) { - $productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $productMock = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); $productMock->expects($this->any())->method('getIsVirtual')->willReturn($type); $itemMock = $this->getMock( - 'Magento\Quote\Model\Quote\Item', + \Magento\Quote\Model\Quote\Item::class, ['isDeleted', 'getParentItemId', 'getProduct'], [], '', @@ -1183,7 +1191,7 @@ public function dataProviderForTestBeforeSaveIsVirtualQuote() public function testGetItemsCollection() { - $itemCollectionMock = $this->getMockBuilder('Magento\Quote\Model\ResourceModel\Quote\Collection') + $itemCollectionMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) ->disableOriginalConstructor() ->setMethods(['setQuote']) ->getMock(); @@ -1194,7 +1202,7 @@ public function testGetItemsCollection() $this->extensionAttributesJoinProcessorMock->expects($this->once()) ->method('process') ->with( - $this->isInstanceOf('Magento\Quote\Model\ResourceModel\Quote\Collection') + $this->isInstanceOf(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) ); $itemCollectionMock->expects($this->once())->method('setQuote')->with($this->quote); @@ -1203,7 +1211,7 @@ public function testGetItemsCollection() public function testGetAllItems() { - $itemOneMock = $this->getMockBuilder('Magento\Quote\Model\ResourceModel\Quote\Item') + $itemOneMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Item::class) ->setMethods(['isDeleted']) ->disableOriginalConstructor() ->getMock(); @@ -1211,7 +1219,7 @@ public function testGetAllItems() ->method('isDeleted') ->willReturn(false); - $itemTwoMock = $this->getMockBuilder('Magento\Quote\Model\ResourceModel\Quote\Item') + $itemTwoMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Item::class) ->setMethods(['isDeleted']) ->disableOriginalConstructor() ->getMock(); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index f2ce59cc091d2..72a4bde0055e6 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1179,5 +1179,65 @@ overnight-duffle + + + default + + Simple Product %isolation% + sku_simple_product_%isolation% + This item has weight + 1 + + 1 + In Stock + + + 560 + + + taxable_goods + + + + default + + + Catalog, Search + simple-product-%isolation% + + simple_order_default + + + + + + default + + Simple Product %isolation% + sku_simple_product_%isolation% + This item has weight + 10 + + 25 + In Stock + + + 560 + + + taxable_goods + + + + default + + + Catalog, Search + simple-product-%isolation% + + simple_order_default + + + diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Order/History.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Order/History.php index ab2e578680b4d..38024301a482d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Order/History.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Order/History.php @@ -43,6 +43,13 @@ class History extends Block */ protected $viewButton = '.action.view'; + /** + * 'Reorder' button css selector. + * + * @var string + */ + protected $reorderButton = '.action.order'; + /** * Order history form selector. * @@ -59,6 +66,7 @@ class History extends Block public function isOrderVisible($order) { $this->waitFormToLoad(); + return $this->_rootElement->find( sprintf($this->customerOrders, $order['id'], $order['status']), Locator::SELECTOR_XPATH @@ -74,6 +82,7 @@ public function isOrderVisible($order) public function getOrderTotalById($id) { $this->waitFormToLoad(); + return $this->escapeCurrency($this->searchOrderById($id)->find($this->total)->getText()); } @@ -100,6 +109,19 @@ public function openOrderById($id) $this->searchOrderById($id)->find($this->viewButton)->click(); } + /** + * Check if 'Reorder' button is visible for customer on order page. + * + * @param string $id + * @return boolean + */ + public function isReorderButtonPresentByOrderId($id) + { + $this->waitFormToLoad(); + + return $this->searchOrderById($id)->find($this->reorderButton)->isVisible(); + } + /** * Method that escapes currency symbols. * @@ -109,6 +131,7 @@ public function openOrderById($id) protected function escapeCurrency($price) { preg_match("/^\\D*\\s*([\\d,\\.]+)\\s*\\D*$/", $price, $matches); + return (isset($matches[1])) ? $matches[1] : null; } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertReorderButtonIsNotVisibleOnFrontend.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertReorderButtonIsNotVisibleOnFrontend.php new file mode 100644 index 0000000000000..7717517648fbc --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertReorderButtonIsNotVisibleOnFrontend.php @@ -0,0 +1,82 @@ + $order->hasData('id') ? $order->getId() : $orderId, + 'status' => $statusToCheck === null ? $status : $statusToCheck, + ]; + + /** @var \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep $loginStep */ + $loginStep = $testStepFactory->create( + \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep::class, + ['customer' => $customer] + ); + $loginStep->run(); + + $customerAccountIndex->getAccountMenuBlock()->openMenuItem('My Orders'); + $errorMessage = implode(', ', $filter); + + \PHPUnit_Framework_Assert::assertFalse( + $orderHistory->getOrderHistoryBlock()->isReorderButtonPresentByOrderId($filter['id']), + '"Reorder" button for order with following data \'' . $errorMessage + . '\' is present in Orders block on frontend.' + ); + + $loginStep->cleanup(); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return '"Reorder" button is not present in orders on frontend.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml index 90c65091c117e..3e2310322fefb 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml @@ -148,7 +148,7 @@ - + test_type:acceptance_test catalogProductSimple::default active_sales_rule_free_shipping @@ -165,5 +165,21 @@ + + catalogProductSimple::default_qty_1 + default + US_address_1_without_email + No + Flat Rate + Fixed + + 565.00 + + cashondelivery + cashondelivery + + + + diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products.php index 1e5083383db75..7788177dcad80 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products.php @@ -4,9 +4,11 @@ * See COPYING.txt for license details. */ +use Magento\TestFramework\Helper\Bootstrap; + /** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product'); +$product = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\Product::class); $product ->setTypeId('simple') ->setId(1) @@ -23,8 +25,8 @@ ->setStockData(['use_config_manage_stock' => 0]) ->save(); -$customDesignProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product', ['data' => $product->getData()]); +$customDesignProduct = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\Product::class, ['data' => $product->getData()]); $customDesignProduct->setUrlKey('custom-design-simple-product') ->setId(2) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_rollback.php index 9a0626999fdd6..1da9757d58279 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_rollback.php @@ -3,9 +3,10 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\TestFramework\Helper\Bootstrap; /** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); @@ -13,8 +14,8 @@ /** * @var Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get('Magento\Catalog\Api\ProductRepositoryInterface'); +$productRepository = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); try { $product = $productRepository->get('simple', false, null, true); $productRepository->delete($product); @@ -29,5 +30,13 @@ //Product already removed } +// Remove product stock registry data. +/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = Bootstrap::getObjectManager()->get( + \Magento\CatalogInventory\Model\StockRegistryStorage::class +); +$stockRegistryStorage->removeStockItem(1); +$stockRegistryStorage->removeStockStatus(1); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_product_and_payment_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_product_and_payment_rollback.php new file mode 100644 index 0000000000000..6fe7cc75a7037 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_product_and_payment_rollback.php @@ -0,0 +1,7 @@ +loadArea( +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea( \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE ); /** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); -$product->setTypeId( - 'virtual' -)->setId( - 1 -)->setAttributeSetId( - 4 -)->setName( - 'Simple Product' -)->setSku( - 'simple' -)->setPrice( - 10 -)->setStoreId( - 1 -)->setStockData( - ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 100] -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->save(); +$product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('virtual') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setStoreId(1) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 100 + ] + ) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); $product->load(1); /** @var $quote \Magento\Quote\Model\Quote */ -$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); +$quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quoteItem = $quote->setCustomerId( 1 )->setStoreId( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Store\Model\StoreManagerInterface' + \Magento\Store\Model\StoreManagerInterface::class )->getStore()->getId() )->setReservedOrderId( 'test01' diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/quote_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/quote_rollback.php new file mode 100644 index 0000000000000..bb97f7ac8c47e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/quote_rollback.php @@ -0,0 +1,41 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->load('test01', 'reserved_order_id'); +if ($quote->getId()) { + $quote->delete(); +} + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +// Remove product stock registry data. +/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = Bootstrap::getObjectManager()->get( + \Magento\CatalogInventory\Model\StockRegistryStorage::class +); +$stockRegistryStorage->removeStockItem(1); +$stockRegistryStorage->removeStockStatus(1); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 035e8b82042f6..f87ecdb5eae01 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -7,13 +7,23 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Exception\LocalizedException; +use Magento\Catalog\Model\ProductRepository; +/** + * Test quote + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class QuoteTest extends \PHPUnit_Framework_TestCase { + /** + * @param $entity + * @return mixed + */ private function convertToArray($entity) { return Bootstrap::getObjectManager() - ->create('Magento\Framework\Api\ExtensibleDataObjectConverter') + ->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class) ->toFlatArray($entity); } @@ -23,11 +33,13 @@ private function convertToArray($entity) */ public function testCollectTotalsWithVirtual() { - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->load('test01', 'reserved_order_id'); - $productRepository = Bootstrap::getObjectManager()->create('Magento\Catalog\Api\ProductRepositoryInterface'); - $product = $productRepository->get('virtual-product'); + $productRepository = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + $product = $productRepository->get('virtual-product', false, null, true); $quote->addProduct($product); $quote->collectTotals(); @@ -40,20 +52,23 @@ public function testCollectTotalsWithVirtual() public function testSetCustomerData() { /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); - /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory */ - $customerFactory = Bootstrap::getObjectManager()->create('Magento\Customer\Api\Data\CustomerInterfaceFactory'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); + /** @var CustomerInterfaceFactory $customerFactory */ + $customerFactory = Bootstrap::getObjectManager()->create( + CustomerInterfaceFactory::class + ); /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = Bootstrap::getObjectManager()->create('Magento\Framework\Api\DataObjectHelper'); + $dataObjectHelper = Bootstrap::getObjectManager()->create( + \Magento\Framework\Api\DataObjectHelper::class + ); $expected = $this->_getCustomerDataArray(); $customer = $customerFactory->create(); $dataObjectHelper->populateWithArray( $customer, $expected, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); - $this->assertEquals($expected, $this->convertToArray($customer)); $quote->setCustomer($customer); // @@ -65,10 +80,14 @@ public function testSetCustomerData() public function testUpdateCustomerData() { /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); - $customerFactory = Bootstrap::getObjectManager()->create('Magento\Customer\Api\Data\CustomerInterfaceFactory'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); + $customerFactory = Bootstrap::getObjectManager()->create( + CustomerInterfaceFactory::class + ); /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = Bootstrap::getObjectManager()->create('Magento\Framework\Api\DataObjectHelper'); + $dataObjectHelper = Bootstrap::getObjectManager()->create( + \Magento\Framework\Api\DataObjectHelper::class + ); $expected = $this->_getCustomerDataArray(); //For save in repository $expected = $this->removeIdFromCustomerData($expected); @@ -76,14 +95,14 @@ public function testUpdateCustomerData() $dataObjectHelper->populateWithArray( $customerDataSet, $expected, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $this->assertEquals($expected, $this->convertToArray($customerDataSet)); /** * @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ $customerRepository = Bootstrap::getObjectManager() - ->create('Magento\Customer\Api\CustomerRepositoryInterface'); + ->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); $customerRepository->save($customerDataSet); $quote->setCustomer($customerDataSet); $expected = $this->_getCustomerDataArray(); @@ -92,7 +111,7 @@ public function testUpdateCustomerData() $dataObjectHelper->populateWithArray( $customerDataUpdated, $expected, - '\Magento\Customer\Api\Data\CustomerInterface' + \Magento\Customer\Api\Data\CustomerInterface::class ); $quote->updateCustomerData($customerDataUpdated); $customer = $quote->getCustomer(); @@ -110,17 +129,23 @@ public function testUpdateCustomerData() public function testGetCustomerGroupFromCustomer() { /** Preconditions */ - /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory */ - $customerFactory = Bootstrap::getObjectManager()->create('Magento\Customer\Api\Data\CustomerInterfaceFactory'); + /** @var CustomerInterfaceFactory $customerFactory */ + $customerFactory = Bootstrap::getObjectManager()->create( + CustomerInterfaceFactory::class + ); $customerGroupId = 3; $customerData = $customerFactory->create()->setId(1)->setGroupId($customerGroupId); /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->setCustomer($customerData); $quote->unsetData('customer_group_id'); /** Execute SUT */ - $this->assertEquals($customerGroupId, $quote->getCustomerGroupId(), "Customer group ID is invalid"); + $this->assertEquals( + $customerGroupId, + $quote->getCustomerGroupId(), + "Customer group ID is invalid" + ); } /** @@ -134,14 +159,18 @@ public function testGetCustomerTaxClassId() $fixtureGroupCode = 'custom_group'; $fixtureTaxClassId = 3; /** @var \Magento\Customer\Model\Group $group */ - $group = Bootstrap::getObjectManager()->create('Magento\Customer\Model\Group'); + $group = Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Group::class); $fixtureGroupId = $group->load($fixtureGroupCode, 'customer_group_code')->getId(); /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->setCustomerGroupId($fixtureGroupId); /** Execute SUT */ - $this->assertEquals($fixtureTaxClassId, $quote->getCustomerTaxClassId(), 'Customer tax class ID is invalid.'); + $this->assertEquals( + $fixtureTaxClassId, + $quote->getCustomerTaxClassId(), + 'Customer tax class ID is invalid.' + ); } /** @@ -158,7 +187,7 @@ public function testAssignCustomerWithAddressChangeAddressesNotSpecified() * First address is default billing, second is default shipping. */ /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); /** Execute SUT */ @@ -224,7 +253,7 @@ public function testAssignCustomerWithAddressChange() */ /** @var \Magento\Quote\Model\Quote $quote */ $objectManager = Bootstrap::getObjectManager(); - $quote = $objectManager->create('Magento\Quote\Model\Quote'); + $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); /** @var \Magento\Quote\Model\Quote\Address $quoteBillingAddress */ $expectedBillingAddressData = [ @@ -237,7 +266,7 @@ public function testAssignCustomerWithAddressChange() 'firstname' => 'FirstBilling', 'region_id' => 1 ]; - $quoteBillingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); + $quoteBillingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); $quoteBillingAddress->setData($expectedBillingAddressData); $expectedShippingAddressData = [ @@ -250,7 +279,7 @@ public function testAssignCustomerWithAddressChange() 'firstname' => 'FirstShipping', 'region_id' => 1 ]; - $quoteShippingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); + $quoteShippingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); $quoteShippingAddress->setData($expectedShippingAddressData); /** Execute SUT */ @@ -258,7 +287,11 @@ public function testAssignCustomerWithAddressChange() /** Check if SUT caused expected effects */ $fixtureCustomerId = 1; - $this->assertEquals($fixtureCustomerId, $quote->getCustomerId(), 'Customer ID in quote is invalid.'); + $this->assertEquals( + $fixtureCustomerId, + $quote->getCustomerId(), + 'Customer ID in quote is invalid.' + ); $billingAddress = $quote->getBillingAddress(); foreach ($expectedBillingAddressData as $field => $value) { @@ -284,13 +317,15 @@ public function testAssignCustomerWithAddressChange() public function testAddProductUpdateItem() { /** @var \Magento\Quote\Model\Quote $quote */ - $quote = Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); + $quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->load('test01', 'reserved_order_id'); $productStockQty = 100; - $productRepository = Bootstrap::getObjectManager()->create('Magento\Catalog\Api\ProductRepositoryInterface'); - $product = $productRepository->get('simple-1'); + $productRepository = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + $product = $productRepository->get('simple-1', false, null, true); $quote->addProduct($product, 50); $quote->setTotalsCollectedFlag(false)->collectTotals(); @@ -310,7 +345,7 @@ public function testAddProductUpdateItem() $this->assertEquals(1, $quote->getItemsQty()); $this->setExpectedException( - '\Magento\Framework\Exception\LocalizedException', + \Magento\Framework\Exception\LocalizedException::class, 'We don\'t have as many "Simple Product" as you requested.' ); $updateParams['qty'] = $productStockQty + 1; @@ -329,10 +364,12 @@ protected function _prepareQuoteForTestAssignCustomerWithAddressChange($quote) { $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create('Magento\Customer\Api\CustomerRepositoryInterface'); + $customerRepository = $objectManager->create( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); $fixtureCustomerId = 1; /** @var \Magento\Customer\Model\Customer $customer */ - $customer = $objectManager->create('Magento\Customer\Model\Customer'); + $customer = $objectManager->create(\Magento\Customer\Model\Customer::class); $fixtureSecondAddressId = 2; $customer->load($fixtureCustomerId)->setDefaultShipping($fixtureSecondAddressId)->save(); $customerData = $customerRepository->getById($fixtureCustomerId); @@ -368,6 +405,9 @@ protected function removeIdFromCustomerData(array $customerData) return $customerData; } + /** + * @return array + */ protected function _getCustomerDataArray() { return [ @@ -381,7 +421,7 @@ protected function _getCustomerDataArray() \Magento\Customer\Model\Data\Customer::FIRSTNAME => 'Joe', \Magento\Customer\Model\Data\Customer::GENDER => 'Male', \Magento\Customer\Model\Data\Customer::GROUP_ID => - \Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID, + \Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID, \Magento\Customer\Model\Data\Customer::ID => 1, \Magento\Customer\Model\Data\Customer::LASTNAME => 'Dou', \Magento\Customer\Model\Data\Customer::MIDDLENAME => 'Ivan', @@ -392,4 +432,29 @@ protected function _getCustomerDataArray() \Magento\Customer\Model\Data\Customer::WEBSITE_ID => 1 ]; } + + /** + * Test to verify that disabled product cannot be added to cart. + * + * @magentoDataFixture Magento/Quote/_files/is_not_salable_product.php + */ + public function testAddedProductToQuoteIsSalable() + { + $productId = 99; + $objectManager = Bootstrap::getObjectManager(); + + /** @var ProductRepository $productRepository */ + $productRepository = $objectManager->get(ProductRepository::class); + + /** @var \Magento\Quote\Model\Quote $quote */ + $product = $productRepository->getById($productId, false, null, true); + + $this->setExpectedException( + LocalizedException::class, + 'Product that you are trying to add is not available.' + ); + + $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); + $quote->addProduct($product); + } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product.php b/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product.php new file mode 100644 index 0000000000000..27b3df1bf673a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product.php @@ -0,0 +1,40 @@ +create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(99) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple-99') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product_rollback.php new file mode 100644 index 0000000000000..f352e7c7ba44f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/_files/is_not_salable_product_rollback.php @@ -0,0 +1,26 @@ +get('Magento\Framework\Registry'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple-99', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php index f81b29356432f..2bb3dd4545a99 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php @@ -3,8 +3,11 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea(Magento\Framework\App\Area::AREA_FRONTEND); + +$product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); $product->setTypeId('simple') ->setId(1) ->setAttributeSetId(4) @@ -22,15 +25,16 @@ 'qty' => 100, 'is_in_stock' => 1, ] - )->save(); + ); -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Api\ProductRepositoryInterface'); -$product = $productRepository->get('simple'); +/** @var Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$product = $productRepository->save($product); $addressData = include __DIR__ . '/address_data.php'; -$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Quote\Model\Quote\Address', +$billingAddress = Bootstrap::getObjectManager()->create( + \Magento\Quote\Model\Quote\Address::class, ['data' => $addressData] ); $billingAddress->setAddressType('billing'); @@ -38,12 +42,13 @@ $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType('shipping'); +/** @var \Magento\Store\Api\Data\StoreInterface $store */ $store = Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get('Magento\Store\Model\StoreManagerInterface') + ->get(\Magento\Store\Model\StoreManagerInterface::class) ->getStore(); /** @var \Magento\Quote\Model\Quote $quote */ -$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); +$quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->setCustomerIsGuest(true) ->setStoreId($store->getId()) ->setReservedOrderId('test01') @@ -56,8 +61,8 @@ $quote->save(); /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ -$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Quote\Model\QuoteIdMaskFactory') +$quoteIdMask = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) ->create(); $quoteIdMask->setQuoteId($quote->getId()); $quoteIdMask->setDataChanges(true); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_rollback.php index 2fcbc32b49bcd..72769f75e476a 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_rollback.php @@ -3,22 +3,23 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\TestFramework\Helper\Bootstrap; /** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); /** @var $quote \Magento\Quote\Model\Quote */ -$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote'); +$quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); $quote->load('test01', 'reserved_order_id'); if ($quote->getId()) { $quote->delete(); } /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Api\ProductRepositoryInterface'); +$productRepository = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); try { $product = $productRepository->get('simple', false, null, true); @@ -27,5 +28,13 @@ //Product already removed } +// Remove product stock registry data. +/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = Bootstrap::getObjectManager()->get( + \Magento\CatalogInventory\Model\StockRegistryStorage::class +); +$stockRegistryStorage->removeStockItem(1); +$stockRegistryStorage->removeStockStatus(1); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); From 460c3bf6bd3668564e1152b0af60cbd4c0ad7764 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 24 May 2017 09:48:38 +0300 Subject: [PATCH 105/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- .../Form/Modifier/AdvancedInventory.php | 1 - .../adminhtml/ui_component/product_form.xml | 1 - .../js/components/qty-validator-changer.js | 3 +- .../view/base/web/js/lib/validation/rules.js | 9 + .../Constraint/AssertProductSuccessUpdate.php | 155 ++++++++++++++++++ .../Test/Repository/CatalogProductSimple.xml | 30 ++++ .../CatalogProductSimple/CheckoutData.xml | 8 + .../Catalog/Test/Repository/ConfigData.xml | 16 ++ .../Product/CreateSimpleProductEntityTest.xml | 12 ++ 9 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php 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 94ded128ec06f..051dbbc1d8701 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 @@ -213,7 +213,6 @@ private function prepareMeta() 'dataScope' => 'qty', 'validation' => [ 'validate-number' => true, - 'validate-digits' => true, 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, ], 'imports' => [ 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 af7ad35e0df54..f1e867b917e6b 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 @@ -95,7 +95,6 @@ quantity_and_stock_status.qty true - true 99999999 200 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 f11e020e72337..3123784e64e71 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 @@ -19,8 +19,7 @@ define([ handleChanges: function (value) { var isDigits = value !== 1; - this.validation['validate-number'] = !isDigits; - this.validation['validate-digits'] = isDigits; + this.validation['validate-integer'] = isDigits; this.validation['less-than-equals-to'] = isDigits ? 99999999 : 99999999.9999; this.validate(); } 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 c78f253c8aec0..da19387d6867d 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 @@ -572,6 +572,15 @@ define([ }, $.mage.__('Please enter a valid number in this field.') ], + 'validate-integer': [ + function(value) { + return ( + utils.isEmptyNoTrim(value) + || (!isNaN(utils.parseNumber(value)) && /^\s*-?\d*\s*$/.test(value)) + ); + }, + $.mage.__('Please enter a valid integer in this field.') + ], "validate-number-range": [ function(value, param) { if (utils.isEmptyNoTrim(value)) { diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php new file mode 100644 index 0000000000000..8044a5c587863 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php @@ -0,0 +1,155 @@ +testStepFactory = $testStepFactory; + $this->productGrid = $productGrid; + $this->editProductPage = $editProductPage; + + // Create new order for guest. + $this->placeOrder($product, $shippingAddress, $shipping, $payment); + + //Update product. + $this->updateProduct($product, $updatedProduct); + + $actualMessages = $this->editProductPage->getMessagesBlock()->getSuccessMessages(); + \PHPUnit_Framework_Assert::assertContains( + self::SUCCESS_MESSAGE, + $actualMessages, + 'Wrong success message is displayed.' + . "\nExpected: " . self::SUCCESS_MESSAGE + . "\nActual:\n" . implode("\n - ", $actualMessages) + ); + } + + /** + * Place order. + * + * @param CatalogProductSimple $product + * @param Address $address + * @param array $shipping + * @param array $payment + */ + private function placeOrder( + CatalogProductSimple $product, + Address $address, + array $shipping, + array $payment + ) { + // Add products to cart. + $this->testStepFactory->create(AddProductsToTheCartStep::class, ['products' => [$product]])->run(); + // Proceed to checkout. + $this->testStepFactory->create(ProceedToCheckoutStep::class)->run(); + // Fill shipping address. + $this->testStepFactory->create(FillShippingAddressStep::class, ['shippingAddress' => $address])->run(); + // Select shipping method. + $this->testStepFactory->create(FillShippingMethodStep::class, ['shipping' => $shipping])->run(); + // Select payment method. + $this->testStepFactory->create(SelectPaymentMethodStep::class, ['payment' => $payment])->run(); + // Click "Place order" button. + $result = $this->testStepFactory->create(PlaceOrderStep::class)->run(); + + \PHPUnit_Framework_Assert::assertNotEmpty( + $result['orderId'], + 'Order not placed.' + ); + } + + /** + * Update product. + * + * @param CatalogProductSimple $product + * @param CatalogProductSimple $updatedProduct + */ + private function updateProduct( + CatalogProductSimple $product, + CatalogProductSimple $updatedProduct + ) { + $filter = ['sku' => $product->getSku()]; + + $this->productGrid->open(); + $this->productGrid->getProductGrid()->searchAndOpen($filter); + $this->editProductPage->getProductForm()->fill($updatedProduct); + $this->editProductPage->getFormPageActions()->save(); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Assertion that product success save message is present.'; + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index f2ce59cc091d2..96995ce1b0e28 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -37,6 +37,36 @@ + + + default + + Simple Product %isolation% + sku_simple_product_%isolation% + This item has weight + 1 + + 1 + In Stock + + + 560 + + + taxable_goods + + + + default + + + Catalog, Search + simple-product-%isolation% + + simple_order_backorders + + + default diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml index c06c8b04ef8e0..4d946cbc405a7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml @@ -101,6 +101,14 @@ + + 3 + + 560 + 1680 + + + 2 diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml index 3b942758eeb8c..878ca72d574e3 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/ConfigData.xml @@ -50,5 +50,21 @@ 0 + + + default + 0 + Allow Qty Below 0 + 1 + + + + + default + 0 + No Backorders + 0 + + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index 4650c2057546e..f35d590a41d23 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -466,5 +466,17 @@ + + enable_backorders + simple_with_qty_1 + US_address_1 + Flat Rate + Fixed + checkmo + Qty of this product now has to be less than 0 + + + + From 66aaa9a3ac19f3b590afec64174819288b178889 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 24 May 2017 10:51:12 +0300 Subject: [PATCH 106/363] MAGETWO-60548: [Backport] - Advanced Pricing is very slow to import - for 2.1 --- .../Model/Import/AdvancedPricing.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 821f9619b1a79..ffe49d9c151b6 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -542,19 +542,24 @@ protected function retrieveOldSkus() */ protected function processCountExistingPrices($prices, $table) { + $oldSkus = $this->retrieveOldSkus(); + $existProductIds = array_intersect_key($oldSkus, $prices); + if (!count($existProductIds)) { + return $this; + } + $tableName = $this->_resourceFactory->create()->getTable($table); $productEntityLinkField = $this->getProductEntityLinkField(); $existingPrices = $this->_connection->fetchAssoc( $this->_connection->select()->from( $tableName, ['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id'] - ) + )->where($productEntityLinkField . ' IN (?)', $existProductIds) ); - $oldSkus = $this->retrieveOldSkus(); foreach ($existingPrices as $existingPrice) { - foreach ($oldSkus as $sku => $productId) { - if ($existingPrice[$productEntityLinkField] == $productId && isset($prices[$sku])) { - $this->incrementCounterUpdated($prices[$sku], $existingPrice); + foreach ($prices as $sku => $skuPrices) { + if (isset($oldSkus[$sku]) && $existingPrice[$productEntityLinkField] == $oldSkus[$sku]) { + $this->incrementCounterUpdated($skuPrices, $existingPrice); } } } From cd0fd6d5d3ab10731e9df9437a379ba130653fc2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 24 May 2017 11:11:34 +0300 Subject: [PATCH 107/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- .../Constraint/AssertProductSuccessUpdate.php | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php index 8044a5c587863..87c8b307e807a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php @@ -7,8 +7,6 @@ namespace Magento\Catalog\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; -use Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit; -use Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex; use Magento\Checkout\Test\TestStep\AddProductsToTheCartStep; use Magento\Checkout\Test\TestStep\FillShippingAddressStep; use Magento\Checkout\Test\TestStep\FillShippingMethodStep; @@ -16,7 +14,6 @@ use Magento\Checkout\Test\TestStep\ProceedToCheckoutStep; use Magento\Checkout\Test\TestStep\SelectPaymentMethodStep; use Magento\Mtf\Constraint\AbstractConstraint; -use Magento\Mtf\TestStep\TestStepFactory; use Magento\Customer\Test\Fixture\Address; /** @@ -32,21 +29,21 @@ class AssertProductSuccessUpdate extends AbstractConstraint /** * Test step factory. * - * @var TestStepFactory + * @var \Magento\Mtf\TestStep\TestStepFactory */ private $testStepFactory; /** * Product page with a grid. * - * @var CatalogProductIndex + * @var \Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex */ protected $productGrid; /** * Page to update a product. * - * @var CatalogProductEdit + * @var \Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit */ protected $editProductPage; @@ -55,9 +52,9 @@ class AssertProductSuccessUpdate extends AbstractConstraint * * @param CatalogProductSimple $product * @param Address $shippingAddress - * @param TestStepFactory $testStepFactory - * @param CatalogProductIndex $productGrid - * @param CatalogProductEdit $editProductPage + * @param \Magento\Mtf\TestStep\TestStepFactory $testStepFactory + * @param \Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex $productGrid + * @param \Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit $editProductPage * @param array $shipping * @param array $payment * @param CatalogProductSimple $updatedProduct @@ -65,9 +62,9 @@ class AssertProductSuccessUpdate extends AbstractConstraint public function processAssert( CatalogProductSimple $product, Address $shippingAddress, - TestStepFactory $testStepFactory, - CatalogProductIndex $productGrid, - CatalogProductEdit $editProductPage, + \Magento\Mtf\TestStep\TestStepFactory $testStepFactory, + \Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex $productGrid, + \Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit $editProductPage, array $shipping, array $payment, CatalogProductSimple $updatedProduct @@ -152,4 +149,4 @@ public function toString() { return 'Assertion that product success save message is present.'; } -} \ No newline at end of file +} From e430fde562c4762c2b520031532a4abdae5541af Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 24 May 2017 13:57:35 +0300 Subject: [PATCH 108/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- ...php => AssertBackorderProductSuccessUpdate.php} | 14 ++++++++++---- .../Product/CreateSimpleProductEntityTest.xml | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) rename dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/{AssertProductSuccessUpdate.php => AssertBackorderProductSuccessUpdate.php} (93%) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php similarity index 93% rename from dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php rename to dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php index 87c8b307e807a..46b421be929ff 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductSuccessUpdate.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php @@ -17,9 +17,9 @@ use Magento\Customer\Test\Fixture\Address; /** - * Assert that product with qty<0 can be updated. + * Assert that backorder product can be updated. */ -class AssertProductSuccessUpdate extends AbstractConstraint +class AssertBackorderProductSuccessUpdate extends AbstractConstraint { /** * Text value to be checked. @@ -48,7 +48,7 @@ class AssertProductSuccessUpdate extends AbstractConstraint protected $editProductPage; /** - * Assert that product with qty<0 can be updated. + * Assert that backorder product can be updated. * * @param CatalogProductSimple $product * @param Address $shippingAddress @@ -58,6 +58,8 @@ class AssertProductSuccessUpdate extends AbstractConstraint * @param array $shipping * @param array $payment * @param CatalogProductSimple $updatedProduct + * + * @return void */ public function processAssert( CatalogProductSimple $product, @@ -96,6 +98,8 @@ public function processAssert( * @param Address $address * @param array $shipping * @param array $payment + * + * @return void */ private function placeOrder( CatalogProductSimple $product, @@ -127,6 +131,8 @@ private function placeOrder( * * @param CatalogProductSimple $product * @param CatalogProductSimple $updatedProduct + * + * @return void */ private function updateProduct( CatalogProductSimple $product, @@ -147,6 +153,6 @@ private function updateProduct( */ public function toString() { - return 'Assertion that product success save message is present.'; + return 'Backorder product updated successfully.'; } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index f35d590a41d23..5bb650fcc0a92 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -476,7 +476,7 @@ Qty of this product now has to be less than 0 - + From 0340f338492e5ffe4e5949680663431d91d76126 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 24 May 2017 15:42:18 +0300 Subject: [PATCH 109/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- .../AssertBackorderProductSuccessUpdate.php | 158 ------------------ .../Test/Repository/CatalogProductSimple.xml | 4 +- .../Product/CreateSimpleProductEntityTest.xml | 9 +- 3 files changed, 4 insertions(+), 167 deletions(-) delete mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php deleted file mode 100644 index 46b421be929ff..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertBackorderProductSuccessUpdate.php +++ /dev/null @@ -1,158 +0,0 @@ -testStepFactory = $testStepFactory; - $this->productGrid = $productGrid; - $this->editProductPage = $editProductPage; - - // Create new order for guest. - $this->placeOrder($product, $shippingAddress, $shipping, $payment); - - //Update product. - $this->updateProduct($product, $updatedProduct); - - $actualMessages = $this->editProductPage->getMessagesBlock()->getSuccessMessages(); - \PHPUnit_Framework_Assert::assertContains( - self::SUCCESS_MESSAGE, - $actualMessages, - 'Wrong success message is displayed.' - . "\nExpected: " . self::SUCCESS_MESSAGE - . "\nActual:\n" . implode("\n - ", $actualMessages) - ); - } - - /** - * Place order. - * - * @param CatalogProductSimple $product - * @param Address $address - * @param array $shipping - * @param array $payment - * - * @return void - */ - private function placeOrder( - CatalogProductSimple $product, - Address $address, - array $shipping, - array $payment - ) { - // Add products to cart. - $this->testStepFactory->create(AddProductsToTheCartStep::class, ['products' => [$product]])->run(); - // Proceed to checkout. - $this->testStepFactory->create(ProceedToCheckoutStep::class)->run(); - // Fill shipping address. - $this->testStepFactory->create(FillShippingAddressStep::class, ['shippingAddress' => $address])->run(); - // Select shipping method. - $this->testStepFactory->create(FillShippingMethodStep::class, ['shipping' => $shipping])->run(); - // Select payment method. - $this->testStepFactory->create(SelectPaymentMethodStep::class, ['payment' => $payment])->run(); - // Click "Place order" button. - $result = $this->testStepFactory->create(PlaceOrderStep::class)->run(); - - \PHPUnit_Framework_Assert::assertNotEmpty( - $result['orderId'], - 'Order not placed.' - ); - } - - /** - * Update product. - * - * @param CatalogProductSimple $product - * @param CatalogProductSimple $updatedProduct - * - * @return void - */ - private function updateProduct( - CatalogProductSimple $product, - CatalogProductSimple $updatedProduct - ) { - $filter = ['sku' => $product->getSku()]; - - $this->productGrid->open(); - $this->productGrid->getProductGrid()->searchAndOpen($filter); - $this->editProductPage->getProductForm()->fill($updatedProduct); - $this->editProductPage->getFormPageActions()->save(); - } - - /** - * Returns a string representation of the object. - * - * @return string - */ - public function toString() - { - return 'Backorder product updated successfully.'; - } -} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 96995ce1b0e28..9e993e75e83d8 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -37,7 +37,7 @@ - + default @@ -46,7 +46,7 @@ This item has weight 1 - 1 + -2 In Stock diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index 5bb650fcc0a92..e8d8438a2b698 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -468,15 +468,10 @@ enable_backorders - simple_with_qty_1 - US_address_1 - Flat Rate - Fixed - checkmo - Qty of this product now has to be less than 0 + simple_with_negative_qty - + From 6600107c67dca271f1a9d097e4e4c1ca196aa1bb Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Wed, 24 May 2017 15:45:06 +0300 Subject: [PATCH 110/363] MAGETWO-64512: Magento\Checkout\Test\TestCase\OnePageCheckoutTest fails on variation OnePageCheckoutUspsTestVariation2 --- .../Usps/Test/TestCase/OnePageCheckoutTest.xml | 6 +++--- .../Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml diff --git a/dev/tests/functional/tests/app/Magento/Usps/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Usps/Test/TestCase/OnePageCheckoutTest.xml index 6ea92cc3f5844..be0e211302548 100644 --- a/dev/tests/functional/tests/app/Magento/Usps/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Usps/Test/TestCase/OnePageCheckoutTest.xml @@ -25,14 +25,14 @@ - catalogProductSimple::default, configurableProduct::default, bundleProduct::bundle_fixed_product + catalogProductSimple::default guest default UK_address UK_address United States Postal Service - Priority Mail International - Priority Mail International + Priority Mail Express International Flat Rate Envelope + Priority Mail Express International Flat Rate Envelope checkmo checkmo, usps, shipping_origin_US_CA test_type:3rd_party_test diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml new file mode 100644 index 0000000000000..3323b23c5a9ed --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml @@ -0,0 +1,13 @@ + + + + + + + + From b90e0fe2d638d5fb1533ade0be47c4b503ae2d9b Mon Sep 17 00:00:00 2001 From: Sergey Shvets Date: Wed, 24 May 2017 16:28:21 +0300 Subject: [PATCH 111/363] MAGETWO-68811: [Backport] - Payflow Pro zero amount transaction in wrong currency - for 2.1 --- .../Payflow/Service/Request/SecureToken.php | 3 +- .../Service/Request/SecureTokenTest.php | 103 +++++++++++++++--- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php index c08770ee191fc..7efb940766336 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php @@ -3,6 +3,7 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Paypal\Model\Payflow\Service\Request; use Magento\Framework\Math\Random; @@ -11,7 +12,6 @@ use Magento\Paypal\Model\Payflow\Transparent; use Magento\Paypal\Model\Payflowpro; use Magento\Quote\Model\Quote; -use Magento\Sales\Model\Order\Payment; /** * Class SecureToken @@ -64,6 +64,7 @@ public function requestToken(Quote $quote) $request->setTrxtype(Payflowpro::TRXTYPE_AUTH_ONLY); $request->setVerbosity('HIGH'); $request->setAmt(0); + $request->setCurrency($quote->getBaseCurrencyCode()); $request->setCreatesecuretoken('Y'); $request->setSecuretokenid($this->mathRandom->getUniqueHash()); $request->setReturnurl($this->url->getUrl('paypal/transparent/response')); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php index 77a7591116eda..32cf13fbf73e1 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php @@ -3,6 +3,7 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Paypal\Test\Unit\Model\Payflow\Service\Request; use Magento\Framework\Math\Random; @@ -10,6 +11,8 @@ use Magento\Framework\UrlInterface; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Paypal\Model\PayflowConfig; +use Magento\Quote\Model\Quote; /** * Test class for \Magento\Paypal\Model\Payflow\Service\Request\SecureToken @@ -36,11 +39,16 @@ class SecureTokenTest extends \PHPUnit_Framework_TestCase */ protected $url; + /** @var DataObject */ + private $request; + protected function setUp() { - $this->url = $this->getMock('Magento\Framework\UrlInterface', [], [], '', false); - $this->mathRandom = $this->getMock('Magento\Framework\Math\Random', [], [], '', false); - $this->transparent = $this->getMock('Magento\Paypal\Model\Payflow\Transparent', [], [], '', false); + $this->url = $this->buildMock(UrlInterface::class); + $this->mathRandom = $this->buildMock(Random::class); + $this->request = new DataObject(); + + $this->transparent = $this->buildPaymentService($this->request); $this->model = new SecureToken( $this->url, @@ -49,23 +57,13 @@ protected function setUp() ); } + /** + * Test Request Token + */ public function testRequestToken() { - $request = new DataObject(); $secureTokenID = 'Sdj46hDokds09c8k2klaGJdKLl032ekR'; - $this->transparent->expects($this->once()) - ->method('buildBasicRequest') - ->willReturn($request); - $this->transparent->expects($this->once()) - ->method('fillCustomerContacts'); - $this->transparent->expects($this->once()) - ->method('getConfig') - ->willReturn($this->getMock('Magento\Paypal\Model\PayflowConfig', [], [], '', false)); - $this->transparent->expects($this->once()) - ->method('postRequest') - ->willReturn(new DataObject()); - $this->mathRandom->expects($this->once()) ->method('getUniqueHash') ->willReturn($secureTokenID); @@ -73,10 +71,79 @@ public function testRequestToken() $this->url->expects($this->exactly(3)) ->method('getUrl'); - $quote = $this->getMock('Magento\Quote\Model\Quote', [], [], '', false); + /** @var Quote | \PHPUnit_Framework_MockObject_MockObject $quote */ + $quote = $this->buildMock(Quote::class); + + $this->model->requestToken($quote); + + $this->assertEquals($secureTokenID, $this->request->getSecuretokenid()); + } + + /** + * Test request currency + * + * @dataProvider currencyProvider + * @param $currency + */ + public function testCurrency($currency) + { + /** @var Quote | \PHPUnit_Framework_MockObject_MockObject $quote */ + $quote = $this->buildMock(Quote::class, ['getBaseCurrencyCode']); + $quote->expects(self::atLeastOnce()) + ->method('getBaseCurrencyCode') + ->willReturn($currency); $this->model->requestToken($quote); - $this->assertEquals($secureTokenID, $request->getSecuretokenid()); + $this->assertEquals($currency, $this->request->getCurrency()); + } + + /** + * Builds default mock object + * + * @param string $class className + * @param array|null $methods + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function buildMock($class, array $methods = []) + { + return $this->getMockBuilder($class) + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + } + + /** + * Creates payment method service + * + * @param DataObject $request + * @return Transparent | \PHPUnit_Framework_MockObject_MockObject + */ + private function buildPaymentService(DataObject $request) + { + $service = $this->buildMock(Transparent::class); + $service->expects($this->once()) + ->method('buildBasicRequest') + ->willReturn($request); + $service->expects($this->once()) + ->method('fillCustomerContacts'); + $service->expects($this->once()) + ->method('getConfig') + ->willReturn($this->buildMock(PayflowConfig::class)); + $service->expects($this->once()) + ->method('postRequest') + ->willReturn(new DataObject()); + + return $service; + } + + /** + * DataProvider for testing currency + * + * @return array + */ + public function currencyProvider() + { + return [['GBP'], [null], ['USD']]; } } From 27e0cd80136c8f07e26b77335685d7593ac3c35c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 24 May 2017 17:15:10 +0300 Subject: [PATCH 112/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- .../Test/Repository/CatalogProductSimple/CheckoutData.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml index 4d946cbc405a7..c06c8b04ef8e0 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple/CheckoutData.xml @@ -101,14 +101,6 @@ - - 3 - - 560 - 1680 - - - 2 From c1456eeb88e3d58c4c5399c64be7f1e5a2b4f305 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 25 May 2017 08:46:05 +0300 Subject: [PATCH 113/363] MAGETWO-60599: It's impossible to specify negative value in "Quantity" field for product (GitHub #7401) --- .../TestCase/Product/CreateSimpleProductEntityTest.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index e8d8438a2b698..5bf004c2381fc 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -466,12 +466,18 @@ - + enable_backorders simple_with_negative_qty + + simple_with_negative_qty + + + + From 680442a5188d4a64972d374d787ebd3e265ba186 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Thu, 25 May 2017 12:14:49 +0300 Subject: [PATCH 114/363] MAGETWO-64512: Magento\Checkout\Test\TestCase\OnePageCheckoutTest fails on variation OnePageCheckoutUspsTestVariation2 --- .../Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml deleted file mode 100644 index 3323b23c5a9ed..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64512.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - From 2e153a1e8f9555cdb85b65640de93bf19f1dfb05 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 25 May 2017 12:29:28 +0300 Subject: [PATCH 115/363] MAGETWO-63814: [Backport] - Auto Generated coupon codes not applying - for 2.1 --- .../SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php | 2 +- .../SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php index da9098ef6d8d9..6964b04e5f488 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php @@ -194,7 +194,7 @@ public function processAssert( $couponCode = isset($couponCodes[0]) ? $couponCodes[0]: null; } - if ($salesRule->getCouponCode() || $salesRuleOrigin->getCouponCode() || $couponCode) { + if ($couponCode) { $this->checkoutCart->getDiscountCodesBlock()->applyCouponCode($couponCode); } $this->assert(); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php index b9899fd2de2da..ad26fb7625f1d 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.php @@ -104,7 +104,7 @@ public function __inject( * @param CatalogProductSimple $productForSalesRule1 * @param CatalogProductSimple|null $productForSalesRule2 * @param Customer|null $customer - * @param null $conditionEntity + * @param string|null $conditionEntity * @param array|null $generateCouponsSettings * * @return array From fb319c8e8778b81771b7843dbd4ca212c2d0cc04 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 25 May 2017 13:25:39 +0300 Subject: [PATCH 116/363] MAGETWO-63814: [Backport] - Auto Generated coupon codes not applying - for 2.1 --- .../AssertCartPriceRuleApplying.php | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php index 6964b04e5f488..75f0696ab09a9 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleApplying.php @@ -91,10 +91,17 @@ abstract class AssertCartPriceRuleApplying extends AbstractConstraint /** * Cart prices to compare. * - * @array cartPrice + * @var array */ protected $cartPrice; + /** + * Login customer on frontend step. + * + * @var \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep + */ + private $logInStep; + /** * Implementation assert. * @@ -137,7 +144,7 @@ abstract protected function assert(); * @param int|null $isLoggedIn * @param array $shipping * @param array $cartPrice - * @param array $couponCode + * @param array $couponCodes * @return void * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -198,6 +205,10 @@ public function processAssert( $this->checkoutCart->getDiscountCodesBlock()->applyCouponCode($couponCode); } $this->assert(); + + if ($isLoggedIn) { + $this->logout(); + } } /** @@ -207,10 +218,23 @@ public function processAssert( */ protected function login() { - $this->objectManager->create( - 'Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep', + $this->logInStep = $this->objectManager->create( + \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep::class, ['customer' => $this->customer] - )->run(); + ); + $this->logInStep->run(); + } + + /** + * LogOut customer. + * + * @return void + */ + protected function logout() + { + if ($this->logInStep) { + $this->logInStep->cleanup(); + } } /** From ef2333cc8d995780dd067d51c465bd7cbeae9df4 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 25 May 2017 17:41:39 +0300 Subject: [PATCH 117/363] MAGETWO-63157: [Backport] - Product image gallery look-ups use incorrect ID - for 2.1 --- .../ResourceModel/Product/Collection.php | 16 +++- .../ResourceModel/Product/CollectionTest.php | 84 +++++++++++-------- .../Model/Product/Type/Configurable.php | 36 ++++++-- .../Model/Product/Type/ConfigurableTest.php | 11 ++- 4 files changed, 102 insertions(+), 45 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 66b45112c28db..fadfedc211d6e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -2213,16 +2213,24 @@ public function addMediaGalleryData() $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); $items = $this->getItems(); - $select->where('entity.' . $linkField . ' IN (?)', array_map(function ($item) { - return $item->getId(); - }, $items)); + $select->where( + 'entity.' . $linkField . ' IN (?)', + array_map( + function ($item) use ($linkField) { + return $item->getData($linkField); + }, + $items + ) + ); foreach ($this->getConnection()->fetchAll($select) as $row) { $mediaGalleries[$row[$linkField]][] = $row; } foreach ($items as $item) { - $mediaEntries = isset($mediaGalleries[$item->getId()]) ? $mediaGalleries[$item->getId()] : []; + $mediaEntries = isset($mediaGalleries[$item->getData($linkField)]) + ? $mediaGalleries[$item->getData($linkField)] + : []; $this->getGalleryReadHandler()->addMediaDataToProduct($item, $mediaEntries); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 2fa57eed2b253..37aaae9b1b15a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -10,6 +10,8 @@ /** * Class CollectionTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends \PHPUnit_Framework_TestCase { @@ -53,68 +55,68 @@ class CollectionTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $entityFactory = $this->getMock('Magento\Framework\Data\Collection\EntityFactory', [], [], '', false); - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface') + $entityFactory = $this->getMock(\Magento\Framework\Data\Collection\EntityFactory::class, [], [], '', false); + $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $fetchStrategy = $this->getMockBuilder('Magento\Framework\Data\Collection\Db\FetchStrategyInterface') + $fetchStrategy = $this->getMockBuilder(\Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $eventManager = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + $eventManager = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $eavConfig = $this->getMockBuilder('Magento\Eav\Model\Config') + $eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) ->disableOriginalConstructor() ->getMock(); - $resource = $this->getMockBuilder('Magento\Framework\App\ResourceConnection') + $resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $eavEntityFactory = $this->getMockBuilder('Magento\Eav\Model\EntityFactory') + $eavEntityFactory = $this->getMockBuilder(\Magento\Eav\Model\EntityFactory::class) ->disableOriginalConstructor() ->getMock(); - $resourceHelper = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Helper') + $resourceHelper = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Helper::class) ->disableOriginalConstructor() ->getMock(); - $universalFactory = $this->getMockBuilder('Magento\Framework\Validator\UniversalFactory') + $universalFactory = $this->getMockBuilder(\Magento\Framework\Validator\UniversalFactory::class) ->disableOriginalConstructor() ->getMock(); - $storeManager = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->setMethods(['getStore', 'getId']) ->getMockForAbstractClass(); - $moduleManager = $this->getMockBuilder('Magento\Framework\Module\Manager') + $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); - $catalogProductFlatState = $this->getMockBuilder('Magento\Catalog\Model\Indexer\Product\Flat\State') + $catalogProductFlatState = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Flat\State::class) ->disableOriginalConstructor() ->getMock(); - $scopeConfig = $this->getMockBuilder('Magento\Framework\App\Config\ScopeConfigInterface') + $scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productOptionFactory = $this->getMockBuilder('Magento\Catalog\Model\Product\OptionFactory') + $productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\Product\OptionFactory::class) ->disableOriginalConstructor() ->getMock(); - $catalogUrl = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Url') + $catalogUrl = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Url::class) ->disableOriginalConstructor() ->getMock(); - $localeDate = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime\TimezoneInterface') + $localeDate = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $customerSession = $this->getMockBuilder('Magento\Customer\Model\Session') + $customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() ->getMock(); - $dateTime = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime') + $dateTime = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime::class) ->disableOriginalConstructor() ->getMock(); - $groupManagement = $this->getMockBuilder('Magento\Customer\Api\GroupManagementInterface') + $groupManagement = $this->getMockBuilder(\Magento\Customer\Api\GroupManagementInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->connectionMock = $this->getMockBuilder('Magento\Framework\DB\Adapter\AdapterInterface') + $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->selectMock = $this->getMockBuilder('Magento\Framework\DB\Select') + $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); @@ -147,8 +149,8 @@ protected function setUp() $this->prepareObjectManager([ [ - 'Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation', - $this->getMock('Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation') + \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class, + $this->getMock(\Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class) ], [ \Magento\Catalog\Model\ResourceModel\Product\Gallery::class, @@ -164,7 +166,7 @@ protected function setUp() ] ]); $this->collection = $helper->getObject( - 'Magento\Catalog\Model\ResourceModel\Product\Collection', + \Magento\Catalog\Model\ResourceModel\Product\Collection::class, [ 'entityFactory' => $entityFactory, 'logger' => $logger, @@ -217,14 +219,19 @@ public function testAddProductCategoriesFilter() $this->collection->addCategoriesFilter([$conditionType => $values]); } - public function testAddMediaGalleryData() + /** + * @dataProvider addMediaGalleryDataDataProvider + * @param string $linkField + * @param int $linkFieldId + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddMediaGalleryData($linkField, $linkFieldId) { $attributeId = 42; - $itemId = 4242; - $linkField = 'entity_id'; - $mediaGalleriesMock = [[$linkField => $itemId]]; + $mediaGalleriesMock = [[$linkField => $linkFieldId]]; $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() + ->setMethods(['getData']) ->getMock(); $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -244,13 +251,13 @@ public function testAddMediaGalleryData() $this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId); $this->entityMock->expects($this->once())->method('getAttribute')->willReturn($attributeMock); - $itemMock->expects($this->atLeastOnce())->method('getId')->willReturn($itemId); - $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$itemId]); + $itemMock->expects($this->atLeastOnce())->method('getData')->willReturn($linkFieldId); + $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$linkFieldId]); $this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock); $metadataMock->expects($this->once())->method('getLinkField')->willReturn($linkField); $this->connectionMock->expects($this->once())->method('fetchAll')->with($selectMock)->willReturn( - [['entity_id' => $itemId]] + [[$linkField => $linkFieldId]] ); $this->galleryReadHandlerMock->expects($this->once())->method('addMediaDataToProduct') ->with($itemMock, $mediaGalleriesMock); @@ -258,17 +265,28 @@ public function testAddMediaGalleryData() $this->assertSame($this->collection, $this->collection->addMediaGalleryData()); } + /** + * @return array + */ + public function addMediaGalleryDataDataProvider() + { + return [ + ['entity_id', 4242], + ['row_id', 4] + ]; + } + /** * @param $map */ private function prepareObjectManager($map) { - $objectManagerMock = $this->getMock('Magento\Framework\ObjectManagerInterface'); + $objectManagerMock = $this->getMock(\Magento\Framework\ObjectManagerInterface::class); $objectManagerMock->expects($this->any())->method('getInstance')->willReturnSelf(); $objectManagerMock->expects($this->any()) ->method('get') ->will($this->returnValueMap($map)); - $reflectionClass = new \ReflectionClass('Magento\Framework\App\ObjectManager'); + $reflectionClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); $reflectionProperty = $reflectionClass->getProperty('_instance'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($objectManagerMock); diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index e98f546943ff8..0eb81d8d826d4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock\Status; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; @@ -203,10 +204,6 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param ProductTypeConfigurable $catalogProductTypeConfigurable * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor - * @param \Magento\Framework\Cache\FrontendInterface $cache, - * @param \Magento\Customer\Model\Session $customerSession, - * @param StockRegistryInterface $stockRegistry, - * @param ProductInterfaceFactory $productFactory * @param \Magento\Framework\Cache\FrontendInterface $cache * @param \Magento\Customer\Model\Session $customerSession * @param StockRegistryInterface $stockRegistry @@ -610,7 +607,7 @@ public function getUsedProducts($product, $requiredAttributeIds = null) ) ); $data = unserialize($this->getCache()->load($key)); - if (!empty($data)) { + if (is_array($data)) { $usedProducts = []; foreach ($data as $item) { $productItem = $this->productFactory->create(); @@ -941,9 +938,15 @@ public function getSelectedAttributesInfo($product) $value = $value->getSource()->getOptionText($attributeValue); } else { $value = ''; + $attributeValue = ''; } - $attributes[] = ['label' => $label, 'value' => $value]; + $attributes[] = [ + 'label' => $label, + 'value' => $value, + 'option_id' => $attributeId, + 'option_value' => $attributeValue + ]; } } } @@ -1356,4 +1359,25 @@ public function getSalableUsedProducts(Product $product, $requiredAttributeIds = return $usedSalableProducts; } + + /** + * @inheritdoc + */ + public function isPossibleBuyFromList($product) + { + /** @var bool $isAllCustomOptionsDisplayed */ + $isAllCustomOptionsDisplayed = true; + + foreach ($this->getConfigurableAttributes($product) as $attribute) { + /** @var Attribute $eavAttribute */ + $eavAttribute = $attribute->getProductAttribute(); + + $isAllCustomOptionsDisplayed = ( + $isAllCustomOptionsDisplayed + && $eavAttribute->getData('used_in_product_listing') + ); + } + + return $isAllCustomOptionsDisplayed; + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 68339bdde3acc..75d4e049551f9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -1,6 +1,6 @@ assertEquals( $this->_model->getSelectedAttributesInfo($productMock), - [['label' => 'attr_store_label', 'value' => '']] + [ + [ + 'label' => 'attr_store_label', + 'value' => '', + 'option_id' => 1, + 'option_value' => '' + ] + ] ); } From d977dc3313f294f9ffe7e8a38f4093b894524c70 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 25 May 2017 20:02:47 +0300 Subject: [PATCH 118/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../Magento/Catalog/Block/Product/View/Attributes.php | 8 +++++++- .../Test/Block/Adminhtml/Product/ProductForm.php | 2 ++ .../Magento/Catalog/Test/Block/Product/ListProduct.php | 10 ++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 4fb047d7a922b..2ec831083f675 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -12,8 +12,12 @@ namespace Magento\Catalog\Block\Product\View; use Magento\Catalog\Model\Product; +use Magento\Framework\Phrase; use Magento\Framework\Pricing\PriceCurrencyInterface; +/** + * Product attributes block. + */ class Attributes extends \Magento\Framework\View\Element\Template { /** @@ -58,6 +62,7 @@ public function getProduct() if (!$this->_product) { $this->_product = $this->_coreRegistry->registry('product'); } + return $this->_product; } @@ -86,7 +91,7 @@ public function getAdditionalData(array $excludeAttr = []) $value = $this->priceCurrency->convertAndFormat($value); } - if (is_string($value) && strlen($value)) { + if (($value instanceof Phrase || is_string($value)) && strlen($value)) { $data[$attribute->getAttributeCode()] = [ 'label' => __($attribute->getStoreLabel()), 'value' => $value, @@ -95,6 +100,7 @@ public function getAdditionalData(array $excludeAttr = []) } } } + return $data; } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php index ec8ea66fd9903..9f214b4017424 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php @@ -109,6 +109,8 @@ public function openSection($sectionName) $sectionElement = $this->getContainerElement($sectionName); if ($sectionElement->getAttribute('type') == 'button') { $sectionElement->click(); + // Wait until section animation finished. + $this->waitForElementVisible($this->closeButton); } else { parent::openSection($sectionName); } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ListProduct.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ListProduct.php index 8d5e307ffa15f..dac05f54d0560 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ListProduct.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ListProduct.php @@ -16,6 +16,8 @@ */ class ListProduct extends Block { + // @codingStandardsIgnoreStart + /** * Locator for product item block. * @@ -23,6 +25,8 @@ class ListProduct extends Block */ protected $productItem = './/*[contains(@class,"product-item-link") and normalize-space(text())="%s"]/ancestor::li'; + // @codingStandardsIgnoreEnd + /** * Locator for product item link. * @@ -48,7 +52,7 @@ public function getProductItem(FixtureInterface $product) $locator = sprintf($this->productItem, $product->getName()); return $this->blockFactory->create( - 'Magento\Catalog\Test\Block\Product\ProductList\ProductItem', + \Magento\Catalog\Test\Block\Product\ProductList\ProductItem::class, ['element' => $this->_rootElement->find($locator, Locator::SELECTOR_XPATH)] ); } @@ -77,6 +81,8 @@ public function getProductNames() */ public function getSortByValues() { - return explode("\n", $this->_rootElement->find($this->sorter)->getText()); + $sortValues = $this->_rootElement->find($this->sorter)->getText(); + + return array_filter(array_map("trim", explode("\n", $sortValues))); } } From 0f430ae79dc95c431274d14147a6d8d46b478cca Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 25 May 2017 20:41:55 +0300 Subject: [PATCH 119/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../TestSuite/InjectableTests/MAGETWO-69474.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml new file mode 100644 index 0000000000000..f848588da54fa --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 7ec895235782c4195bc3363eb63fd5a15754930b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 26 May 2017 13:24:24 +0300 Subject: [PATCH 120/363] MAGETWO-57607: [Backport] - Unable to save product with all unchecked values for multiple select attribute - for 2.1 #7687 --- .../Entity/Attribute/Backend/ArrayBackend.php | 2 ++ .../{ArrayTest.php => ArrayBackendTest.php} | 9 ++--- .../Ui/view/base/web/js/form/client.js | 2 +- .../base/web/js/form/element/multiselect.js | 20 ++++++++++- .../Controller/Adminhtml/ProductTest.php | 35 +++++++++++++++++-- lib/web/mage/utils/misc.js | 26 ++++++++++++++ 6 files changed, 85 insertions(+), 9 deletions(-) rename app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/{ArrayTest.php => ArrayBackendTest.php} (82%) diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php index 47a998a95ecb5..c387631e693cd 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php @@ -42,6 +42,8 @@ public function validate($object) $data = $object->getData($attributeCode); if (is_array($data)) { $object->setData($attributeCode, implode(',', array_filter($data))); + } elseif (empty($data)) { + $object->setData($attributeCode, null); } return parent::validate($object); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php similarity index 82% rename from app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayTest.php rename to app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php index 0a8adb7534cde..e454f1f22f7bb 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php @@ -5,7 +5,7 @@ */ namespace Magento\Eav\Test\Unit\Model\Entity\Attribute\Backend; -class ArrayTest extends \PHPUnit_Framework_TestCase +class ArrayBackendTest extends \PHPUnit_Framework_TestCase { /** * @var \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend @@ -20,13 +20,13 @@ class ArrayTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->_attribute = $this->getMock( - 'Magento\Eav\Model\Entity\Attribute', + \Magento\Eav\Model\Entity\Attribute::class, ['getAttributeCode', '__wakeup'], [], '', false ); - $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger = $this->getMock(\Psr\Log\LoggerInterface::class); $this->_model = new \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend($logger); $this->_model->setAttribute($this->_attribute); } @@ -37,9 +37,10 @@ protected function setUp() public function testValidate($data) { $this->_attribute->expects($this->atLeastOnce())->method('getAttributeCode')->will($this->returnValue('code')); - $product = new \Magento\Framework\DataObject(['code' => $data]); + $product = new \Magento\Framework\DataObject(['code' => $data, 'empty' => '']); $this->_model->validate($product); $this->assertEquals('1,2,3', $product->getCode()); + $this->assertEquals(null, $product->getEmpty()); } public static function attributeValueDataProvider() diff --git a/app/code/Magento/Ui/view/base/web/js/form/client.js b/app/code/Magento/Ui/view/base/web/js/form/client.js index 6b4c1a3262273..f1299bb54b352 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/client.js +++ b/app/code/Magento/Ui/view/base/web/js/form/client.js @@ -22,7 +22,7 @@ define([ function beforeSave(data, url, selectorPrefix, messagesClass) { var save = $.Deferred(); - data = utils.serialize(data); + data = utils.serialize(utils.filterFormData(data)); data['form_key'] = window.FORM_KEY; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js b/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js index 37b389dcba027..c37c67c39d092 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js @@ -13,7 +13,10 @@ define([ return Select.extend({ defaults: { size: 5, - elementTmpl: 'ui/form/element/multiselect' + elementTmpl: 'ui/form/element/multiselect', + listens: { + value: 'setDifferedFromDefault setPrepareToSendData' + } }, /** @@ -38,6 +41,21 @@ define([ return _.isString(value) ? value.split(',') : value; }, + /** + * Sets the prepared data to dataSource + * by path, where key is component link to dataSource with + * suffix "-prepared-for-send". + * + * @param {Array} data - current component value + */ + setPrepareToSendData: function (data) { + if (!data.length) { + data = ''; + } + + this.source.set(this.dataScope + '-prepared-for-send', data); + }, + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index ede108bf7bbd4..a5a5e3ff5b118 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Catalog\Model\ProductRepository; + /** * @magentoAppArea adminhtml */ @@ -27,7 +29,7 @@ public function testSaveActionWithDangerRequest() public function testSaveActionAndNew() { $this->getRequest()->setPostValue(['back' => 'new']); - $repository = $this->_objectManager->create('Magento\Catalog\Model\ProductRepository'); + $repository = $this->_objectManager->create(ProductRepository::class); $product = $repository->get('simple'); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/new/')); @@ -43,7 +45,7 @@ public function testSaveActionAndNew() public function testSaveActionAndDuplicate() { $this->getRequest()->setPostValue(['back' => 'duplicate']); - $repository = $this->_objectManager->create('Magento\Catalog\Model\ProductRepository'); + $repository = $this->_objectManager->create(ProductRepository::class); $product = $repository->get('simple'); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/edit/')); @@ -100,7 +102,7 @@ public function testIndexAction() */ public function testEditAction() { - $repository = $this->_objectManager->create('Magento\Catalog\Model\ProductRepository'); + $repository = $this->_objectManager->create(ProductRepository::class); $product = $repository->get('simple'); $this->dispatch('backend/catalog/product/edit/id/' . $product->getEntityId()); $body = $this->getResponse()->getBody(); @@ -119,4 +121,31 @@ public function testEditAction() '"Save & Duplicate" button isn\'t present on Edit Product page' ); } + + /** + * Tests Validate product action. + * + * @magentoDataFixture Magento/Catalog/_files/products_with_multiselect_attribute.php + * + * @return void + */ + public function testValidateAction() + { + $expectedResult = json_encode(['error' => false]); + + $repository = $this->_objectManager->create(ProductRepository::class); + $product = $repository->get('simple_ms_2'); + $data = $product->getData(); + unset($data['multiselect_attribute']); + + $this->getRequest()->setPostValue(['product' => $data]); + $this->dispatch('backend/catalog/product/validate'); + $response = $this->getResponse()->getBody(); + + $this->assertJsonStringEqualsJsonString( + $expectedResult, + $response, + 'Validate action returned incorrect result.' + ); + } } diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index 5e92ab0ecbe07..c8f7a0481f600 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -205,6 +205,32 @@ define([ } return formData; + }, + + /** + * Filters data object. Finds properties with suffix + * and sets their values to properties with the same name without suffix. + * + * @param {Object} data - The data object that should be filtered + * @param {String} suffix - The string by which data object should be filtered + * @param {String} separator - The string that is separator between property and suffix + * + * @returns {Object} Filtered data object + */ + filterFormData: function (data, suffix, separator) { + data = data || {}; + suffix = suffix || 'prepared-for-send'; + separator = separator || '-'; + _.each(data, function (value, key) { + if (_.isObject(value) && !value.length) { + this.filterFormData(value, suffix, separator) + } else if (_.isString(key) && ~key.indexOf(suffix)) { + data[key.split(separator)[0]] = value; + delete data[key]; + } + }, this); + + return data; } }; }); From 6c4cfc25f60d6591aff0b40cd960b55f3390a35d Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 26 May 2017 15:00:30 +0300 Subject: [PATCH 121/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../Block/Product/View/AttributesTest.php | 292 ++++++++++++++++++ .../InjectableTests/MAGETWO-69474.xml | 15 - 2 files changed, 292 insertions(+), 15 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php new file mode 100644 index 0000000000000..7e93222722034 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php @@ -0,0 +1,292 @@ +context = $this->getMockBuilder(\Magento\Catalog\Block\Product\Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->any()) + ->method('getRegistry') + ->willReturn($this->registry); + + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributes', 'hasData']) + ->getMock(); + + $this->block = new \Magento\Catalog\Block\Product\View\Attributes( + $this->context, + $this->registry, + $this->priceCurrency + ); + + $this->getObjectManager()->setBackwardCompatibleProperty($this->block, '_product', $this->product); + } + + /** + * @covers \Magento\Catalog\Block\Product\View\Attributes::getAdditionalData + * @dataProvider getAdditionalDataProvider + * + * @param array $attributes + * @param bool $productHasAttributeValue + * @param array $excludedAttributes + * @param array $expectedResult + * @return void + */ + public function testGetAdditionalData( + $attributes, + $productHasAttributeValue, + $excludedAttributes, + $expectedResult + ) { + $this->product->expects(self::once())->method('getAttributes') + ->willReturn($attributes); + + $this->product->expects(self::any())->method('hasData') + ->with('attribute') + ->willReturn($productHasAttributeValue); + + $this->priceCurrency->expects(self::any()) + ->method('convertAndFormat') + ->withAnyParameters() + ->willReturn('test'); + + self::assertEquals($expectedResult, $this->block->getAdditionalData($excludedAttributes)); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function getAdditionalDataProvider() + { + return [ + 'No Attributes' => [ + [], + false, + [], + [] + ], + 'With Invisible On Frontend Attribute' => [ + [ + $this->prepareAttributeMock(['is_visible_on_front' => false]) + ], + false, + [], + [] + ], + 'With Excluded On Frontend Attribute' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'excluded_attribute', + 'is_visible_on_front' => false + ] + ) + ], + false, + ['excluded_attribute'], + [] + ], + 'Product Has No Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + ] + ) + ], + false, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'N/A', + 'code' => 'attribute', + ] + ] + ], + 'Product With Null Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'value' => null + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'No', + 'code' => 'attribute', + ] + ] + ], + 'Product With Price Attribute' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'frontend_input' => 'price', + 'value' => '2.1' + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'test', + 'code' => 'attribute', + ] + ] + ], + 'Product With Phrase Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'frontend_input' => 'price', + 'value' => __('test') + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'test', + 'code' => 'attribute', + ] + ] + ], + ]; + } + + /** + * Return object manager. + * + * @return ObjectManager + */ + private function getObjectManager() + { + if ($this->objectManager === null) { + $this->objectManager = new ObjectManager($this); + } + + return $this->objectManager; + } + + /** + * Prepare attribute mock. + * + * @param array $data + * @return \Magento\Eav\Model\Entity\Attribute + */ + private function prepareAttributeMock($data = []) + { + $attributeValue = isset($data['value']) ? $data['value']: null; + + /** @var \PHPUnit_Framework_MockObject_MockObject $frontendModel */ + $frontendModel = $this->getMockBuilder( + \Magento\Eav\Model\Entity\Attribute\Frontend\DefaultFrontend::class + ) + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMock(); + + $frontendModel->expects(self::any()) + ->method('getValue') + ->willReturn($attributeValue); + + $attribute = $this->getObjectManager()->getObject( + \Magento\Eav\Model\Entity\Attribute::class, + ['data' => $data] + ); + $this->getObjectManager()->setBackwardCompatibleProperty($attribute, '_frontend', $frontendModel); + + return $attribute; + } +} diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml deleted file mode 100644 index f848588da54fa..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-69474.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From a9a795747a39a3e324f5b96beb5991f1a7bc88e9 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Fri, 26 May 2017 15:33:20 +0300 Subject: [PATCH 122/363] MAGETWO-61267: Change format date for update and add ability to run cron from functional test --- .../Component/Form/Element/DataType/Date.php | 17 +-- .../Form/Element/DataType/DateTest.php | 137 ++++++++++++++++++ .../Ui/view/base/web/js/form/element/date.js | 16 +- dev/tests/functional/.htaccess | 6 + .../lib/Magento/Mtf/Util/Command/Cli/Cron.php | 31 ++++ .../Backend/Test/Fixture/Source/Date.php | 14 +- .../Test/Fixture/CatalogProductSimple.xml | 1 + .../Handler/CatalogProductSimple/Webapi.php | 3 +- .../Test/Repository/CatalogProductSimple.xml | 27 ++++ .../InjectableTests/MAGETWO-61267.xml | 15 ++ dev/tests/functional/utils/command.php | 1 + lib/web/mage/utils/misc.js | 2 + lib/web/moment-timezone-with-data.js | 7 + 13 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php create mode 100644 dev/tests/functional/.htaccess create mode 100644 dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Cron.php create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml create mode 100644 lib/web/moment-timezone-with-data.js diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index 633340eb82489..82698d65e0763 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -60,18 +60,15 @@ public function __construct( public function prepare() { $config = $this->getData('config'); - - if (!isset($config['timeOffset'])) { - $config['timeOffset'] = (new \DateTime( - 'now', - new \DateTimeZone( - $this->localeDate->getConfigTimezone() - ) - ))->getOffset(); + if (!isset($config['storeTimeZone'])) { + $storeTimeZone = $this->localeDate->getConfigTimezone(); + $config['storeTimeZone'] = $storeTimeZone; } - + // Set date format pattern by current locale + $localeDateFormat = $this->localeDate->getDateFormat(); + $config['options']['dateFormat'] = $localeDateFormat; + $config['outputDateFormat'] = $localeDateFormat; $this->setData('config', $config); - parent::prepare(); } diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php new file mode 100644 index 0000000000000..7a9df1a7df0a4 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php @@ -0,0 +1,137 @@ +contextMock = $this->getMock(Context::class, [], [], '', false); + $this->localeDateMock = $this->getMock(TimezoneInterface::class, [], [], '', false); + $this->localeResolverMock = $this->getMock(ResolverInterface::class, [], [], '', false); + $this->objectManagerHelper = new ObjectManager($this); + $this->processorMock = $this->getMock(Processor::class, [], [], '', false); + $this->contextMock->expects($this->any())->method('getProcessor')->willReturn($this->processorMock); + } + + public function testPrepareWithTimeOffset() + { + $this->date = new Date( + $this->contextMock, + $this->localeDateMock, + $this->localeResolverMock, + [], + [ + 'config' => [ + 'timeOffset' => 1, + ], + ] + ); + + $localeDateFormat = 'dd/MM/y'; + + $this->localeDateMock->expects($this->once()) + ->method('getDateFormat') + ->willReturn($localeDateFormat); + + $this->date->prepare(); + + $config = $this->date->getConfig(); + $this->assertTrue(is_array($config)); + + $this->assertArrayHasKey('options', $config); + $this->assertArrayHasKey('dateFormat', $config['options']); + $this->assertEquals($localeDateFormat, $config['options']['dateFormat']); + + $this->assertArrayHasKey('outputDateFormat', $config); + $this->assertEquals($localeDateFormat, $config['outputDateFormat']); + } + + public function testPrepareWithoutTimeOffset() + { + $defaultDateFormat = 'MM/dd/y'; + + $this->date = new Date( + $this->contextMock, + $this->localeDateMock, + $this->localeResolverMock, + [], + [ + 'config' => [ + 'options' => [ + 'dateFormat' => $defaultDateFormat, + ], + 'outputDateFormat' => $defaultDateFormat, + ], + ] + ); + + $localeDateFormat = 'dd/MM/y'; + + $this->localeDateMock->expects($this->once()) + ->method('getDateFormat') + ->willReturn($localeDateFormat); + $this->localeDateMock->expects($this->any()) + ->method('getConfigTimezone') + ->willReturn('America/Los_Angeles'); + + $this->date->prepare(); + + $config = $this->date->getConfig(); + $this->assertTrue(is_array($config)); + + $this->assertArrayHasKey('options', $config); + $this->assertArrayHasKey('dateFormat', $config['options']); + $this->assertEquals($localeDateFormat, $config['options']['dateFormat']); + + $this->assertArrayHasKey('outputDateFormat', $config); + $this->assertEquals($localeDateFormat, $config['outputDateFormat']); + } + + /** + * This tests ensures that userTimeZone is properly saved in the configuration + */ + public function testPrepare() + { + $this->date = $this->objectManagerHelper->getObject( + Date::class, + [ + 'context' => $this->contextMock, + 'localeDate' => $this->localeDateMock, + 'localeResolver' => $this->localeResolverMock + ] + ); + $this->localeDateMock->expects($this->any())->method('getConfigTimezone')->willReturn('America/Chicago'); + $this->date->prepare(); + $configArray = $this->date->getData('config'); + $this->assertEquals('America/Chicago', $configArray['storeTimeZone']); + } +} diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/date.js b/app/code/Magento/Ui/view/base/web/js/form/element/date.js index 1c9f0b9b3e81c..fde7faa72ed7c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/date.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/date.js @@ -5,7 +5,8 @@ define([ 'moment', 'mageUtils', - './abstract' + './abstract', + 'moment-timezone-with-data' ], function (moment, utils, Abstract) { 'use strict'; @@ -13,7 +14,7 @@ define([ defaults: { options: {}, - timeOffset: 0, + storeTimeZone: 'UTC', validationParams: { dateFormat: '${ $.outputDateFormat }' @@ -61,7 +62,7 @@ define([ /** * Date/time value shifted to corresponding timezone - * according to this.timeOffset property. This value + * according to this.storeTimeZone property. This value * will be sent to the server. * * @type {String} @@ -109,7 +110,7 @@ define([ if (value) { if (this.options.showsTime) { - shiftedValue = moment.utc(value).add(this.timeOffset, 'seconds'); + shiftedValue = moment.tz(value, 'UTC').tz(this.storeTimeZone); } else { dateFormat = this.shiftedValue() ? this.outputDateFormat : this.inputDateFormat; @@ -133,12 +134,13 @@ define([ * @param {String} shiftedValue */ onShiftedValueChange: function (shiftedValue) { - var value; + var value, + formattedValue; if (shiftedValue) { if (this.options.showsTime) { - value = moment.utc(shiftedValue, this.pickerDateTimeFormat); - value = value.subtract(this.timeOffset, 'seconds').toISOString(); + formattedValue = moment(shiftedValue).format('YYYY-MM-DD HH:mm'); + value = moment.tz(formattedValue, this.storeTimeZone).tz('UTC').toISOString(); } else { value = moment(shiftedValue, this.pickerDateTimeFormat); value = value.format(this.outputDateFormat); diff --git a/dev/tests/functional/.htaccess b/dev/tests/functional/.htaccess new file mode 100644 index 0000000000000..ce860a0805f4d --- /dev/null +++ b/dev/tests/functional/.htaccess @@ -0,0 +1,6 @@ +########################################### +## Allow access to command.php + + order allow,deny + allow from all + diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Cron.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Cron.php new file mode 100644 index 0000000000000..81e9e3f9f47b0 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/Cron.php @@ -0,0 +1,31 @@ +params['attribute_code'] . '" field'); } - $date = date(str_replace($delta, '', $data['pattern']), $timestamp); + if (isset($data['apply_timezone']) && $data['apply_timezone'] === true) { + + $timezone = ObjectManager::getInstance() + ->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); + $date = new \DateTime(); + $date->setTimestamp($timestamp); + $date->setTimezone(new \DateTimeZone($timezone->getConfigTimezone())); + $date = $date->format(str_replace($delta, '', $data['pattern'])); + } else { + $date = date(str_replace($delta, '', $data['pattern']), $timestamp); + } if (!$date) { $date = date('m/d/Y'); } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml index 5f920b452d3a0..2954ebcbda5e2 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple.xml @@ -88,5 +88,6 @@ + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php index 22d7603949db6..77d48d74e0660 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Webapi.php @@ -94,9 +94,10 @@ public function persist(FixtureInterface $fixture = null) $this->fields = $this->handlerCurl->prepareData($this->fixture); $this->prepareData(); $this->convertData(); + $storeCode = $this->fixture->hasData('store_code') ? $this->fixture->getStoreCode() : 'default'; /** @var CatalogProductSimple $fixture */ - $url = $_ENV['app_frontend_url'] . 'rest/default/V1/products'; + $url = $_ENV['app_frontend_url'] . 'rest/'. $storeCode .'/V1/products'; $this->webapiTransport->write($url, $this->fields, CurlInterface::POST); $encodedResponse = $this->webapiTransport->read(); $response = json_decode($encodedResponse, true); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index e61f007a3f143..05a0018f7f42a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1207,5 +1207,32 @@ + + simple_product_with_category_%isolation% + Simple product with category %isolation% + + 777 + In Stock + + This item has weight + 1 + + default + + + 100 + + + + default_subcategory + + + Main Website + + simple_with_category + simple-product-%isolation% + all + + diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml new file mode 100644 index 0000000000000..ed87b9a3205e2 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index d1bd30d5538d7..54577acd6bc7c 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -8,6 +8,7 @@ 'cache:flush', 'cache:disable', 'cache:enable', + 'cron:run', ]; if (isset($_GET['command'])) { diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index 5e92ab0ecbe07..ab249c9fa3de2 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -38,6 +38,8 @@ define([ 'EEEE': 'dddd', 'EEE': 'ddd', 'e': 'd', + 'yyyy': 'YYYY', + 'yy': 'YY', 'y': 'YYYY', 'a': 'A' }; diff --git a/lib/web/moment-timezone-with-data.js b/lib/web/moment-timezone-with-data.js new file mode 100644 index 0000000000000..6dfd9559f93d4 --- /dev/null +++ b/lib/web/moment-timezone-with-data.js @@ -0,0 +1,7 @@ +//! moment-timezone.js +//! version : 0.5.5 +//! author : Tim Wood +//! license : MIT +//! github.com/moment/moment-timezone +!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["moment"],b):"object"==typeof module&&module.exports?module.exports=b(require("moment")):b(a.moment)}(this,function(a){"use strict";function b(a){return a>96?a-87:a>64?a-29:a-48}function c(a){var c,d=0,e=a.split("."),f=e[0],g=e[1]||"",h=1,i=0,j=1;for(45===a.charCodeAt(0)&&(d=1,j=-1),d;d0?k[0].zone.name:void 0}function q(a){return D&&!a||(D=p()),D}function r(a){return(a||"").toLowerCase().replace(/\//g,"_")}function s(a){var b,c,d,e;for("string"==typeof a&&(a=[a]),b=0;b= 2.6.0. You are using Moment.js "+a.version+". See momentjs.com"),h.prototype={_set:function(a){this.name=a.name,this.abbrs=a.abbrs,this.untils=a.untils,this.offsets=a.offsets,this.population=a.population},_index:function(a){var b,c=+a,d=this.untils;for(b=0;bd&&A.moveInvalidForward&&(b=d),f= 2.9.0. You are using Moment.js "+a.version+"."),a.defaultZone=b?t(b):null,a};var N=a.momentProperties;return"[object Array]"===Object.prototype.toString.call(N)?(N.push("_z"),N.push("_a")):N&&(N._z=null),w({version:"2016f",zones:["Africa/Abidjan|LMT GMT|g.8 0|01|-2ldXH.Q|48e5","Africa/Accra|LMT GMT GHST|.Q 0 -k|012121212121212121212121212121212121212121212121|-26BbX.8 6tzX.8 MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE 1BAk MnE 1C0k MnE 1BAk MnE 1BAk MnE|41e5","Africa/Nairobi|LMT EAT BEAT BEAUT|-2r.g -30 -2u -2J|01231|-1F3Cr.g 3Dzr.g okMu MFXJ|47e5","Africa/Algiers|PMT WET WEST CET CEST|-9.l 0 -10 -10 -20|0121212121212121343431312123431213|-2nco9.l cNb9.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 DA0 Imo0 rd0 De0 9Xz0 1fb0 1ap0 16K0 2yo0 mEp0 hwL0 jxA0 11A0 dDd0 17b0 11B0 1cN0 2Dy0 1cN0 1fB0 1cL0|26e5","Africa/Lagos|LMT WAT|-d.A -10|01|-22y0d.A|17e6","Africa/Bissau|LMT WAT GMT|12.k 10 0|012|-2ldWV.E 2xonV.E|39e4","Africa/Maputo|LMT CAT|-2a.k -20|01|-2GJea.k|26e5","Africa/Cairo|EET EEST|-20 -30|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-1bIO0 vb0 1ip0 11z0 1iN0 1nz0 12p0 1pz0 10N0 1pz0 16p0 1jz0 s3d0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1WL0 rd0 1Rz0 wp0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1qL0 Xd0 1oL0 11d0 1oL0 11d0 1pb0 11d0 1oL0 11d0 1oL0 11d0 1ny0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 WL0 1qN0 Rb0 1wp0 On0 1zd0 Lz0 1EN0 Fb0 c10 8n0 8Nd0 gL0 e10 mn0|15e6","Africa/Casablanca|LMT WET WEST CET|u.k 0 -10 -10|0121212121212121213121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2gMnt.E 130Lt.E rb0 Dd0 dVb0 b6p0 TX0 EoB0 LL0 gnd0 rz0 43d0 AL0 1Nd0 XX0 1Cp0 pz0 dEp0 4mn0 SyN0 AL0 1Nd0 wn0 1FB0 Db0 1zd0 Lz0 1Nf0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uM0 e00 Dc0 11A0 s00 e00 IM0 WM0 mo0 gM0 LA0 WM0 jA0 e00 Rc0 11A0 e00 e00 U00 11A0 8o0 e00 11A0 11A0 5A0 e00 17c0 1fA0 1a00 1a00 1fA0 17c0 1io0 14o0 1lc0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1lc0 14o0 1fA0|32e5","Africa/Ceuta|WET WEST CET CEST|0 -10 -10 -20|010101010101010101010232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-25KN0 11z0 drd0 18o0 3I00 17c0 1fA0 1a00 1io0 1a00 1y7p0 LL0 gnd0 rz0 43d0 AL0 1Nd0 XX0 1Cp0 pz0 dEp0 4VB0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|85e3","Africa/El_Aaiun|LMT WAT WET WEST|Q.M 10 0 -10|01232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1rDz7.c 1GVA7.c 6L0 AL0 1Nd0 XX0 1Cp0 pz0 1cBB0 AL0 1Nd0 wn0 1FB0 Db0 1zd0 Lz0 1Nf0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uM0 e00 Dc0 11A0 s00 e00 IM0 WM0 mo0 gM0 LA0 WM0 jA0 e00 Rc0 11A0 e00 e00 U00 11A0 8o0 e00 11A0 11A0 5A0 e00 17c0 1fA0 1a00 1a00 1fA0 17c0 1io0 14o0 1lc0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1lc0 14o0 1fA0|20e4","Africa/Johannesburg|SAST SAST SAST|-1u -20 -30|012121|-2GJdu 1Ajdu 1cL0 1cN0 1cL0|84e5","Africa/Khartoum|LMT CAT CAST EAT|-2a.8 -20 -30 -30|01212121212121212121212121212121213|-1yW2a.8 1zK0a.8 16L0 1iN0 17b0 1jd0 17b0 1ip0 17z0 1i10 17X0 1hB0 18n0 1hd0 19b0 1gp0 19z0 1iN0 17b0 1ip0 17z0 1i10 18n0 1hd0 18L0 1gN0 19b0 1gp0 19z0 1iN0 17z0 1i10 17X0 yGd0|51e5","Africa/Monrovia|MMT LRT GMT|H.8 I.u 0|012|-23Lzg.Q 29s01.m|11e5","Africa/Ndjamena|LMT WAT WAST|-10.c -10 -20|0121|-2le10.c 2J3c0.c Wn0|13e5","Africa/Tripoli|LMT CET CEST EET|-Q.I -10 -20 -20|012121213121212121212121213123123|-21JcQ.I 1hnBQ.I vx0 4iP0 xx0 4eN0 Bb0 7ip0 U0n0 A10 1db0 1cN0 1db0 1dd0 1db0 1eN0 1bb0 1e10 1cL0 1c10 1db0 1dd0 1db0 1cN0 1db0 1q10 fAn0 1ep0 1db0 AKq0 TA0 1o00|11e5","Africa/Tunis|PMT CET CEST|-9.l -10 -20|0121212121212121212121212121212121|-2nco9.l 18pa9.l 1qM0 DA0 3Tc0 11B0 1ze0 WM0 7z0 3d0 14L0 1cN0 1f90 1ar0 16J0 1gXB0 WM0 1rA0 11c0 nwo0 Ko0 1cM0 1cM0 1rA0 10M0 zuM0 10N0 1aN0 1qM0 WM0 1qM0 11A0 1o00|20e5","Africa/Windhoek|SWAT SAST SAST CAT WAT WAST|-1u -20 -30 -20 -10 -20|012134545454545454545454545454545454545454545454545454545454545454545454545454545454545454545|-2GJdu 1Ajdu 1cL0 1SqL0 9NA0 11D0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 11B0 1nX0 11B0|32e4","America/Adak|NST NWT NPT BST BDT AHST HST HDT|b0 a0 a0 b0 a0 a0 a0 90|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17SX0 8wW0 iB0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cm0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|326","America/Anchorage|CAT CAWT CAPT AHST AHDT YST AKST AKDT|a0 90 90 a0 90 90 90 80|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17T00 8wX0 iA0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cm0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|30e4","America/Port_of_Spain|LMT AST|46.4 40|01|-2kNvR.U|43e3","America/Araguaina|LMT BRT BRST|3c.M 30 20|0121212121212121212121212121212121212121212121212121|-2glwL.c HdKL.c 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 dMN0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 ny10 Lz0|14e4","America/Argentina/Buenos_Aires|CMT ART ARST ART ARST|4g.M 40 30 30 20|0121212121212121212121212121212121212121213434343434343234343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 j3c0 uL0 1qN0 WL0","America/Argentina/Catamarca|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343454343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Cordoba|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343454343234343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 j3c0 uL0 1qN0 WL0","America/Argentina/Jujuy|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|01212121212121212121212121212121212121212134343456543432343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1ze0 TX0 1ld0 WK0 1wp0 TX0 g0p0 10M0 j3c0 uL0","America/Argentina/La_Rioja|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434534343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Qn0 qO0 16n0 Rb0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Mendoza|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|0121212121212121212121212121212121212121213434345656543235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1u20 SL0 1vd0 Tb0 1wp0 TW0 g0p0 10M0 agM0 Op0 7TX0 uL0","America/Argentina/Rio_Gallegos|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343434343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 ako0 7B0 8zb0 uL0","America/Argentina/Salta|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434543432343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 j3c0 uL0","America/Argentina/San_Juan|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|01212121212121212121212121212121212121212134343434534343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Qn0 qO0 16n0 Rb0 1wp0 TX0 g0p0 10M0 ak00 m10 8lb0 uL0","America/Argentina/San_Luis|CMT ART ARST ART ARST WART WARST|4g.M 40 30 30 20 40 30|01212121212121212121212121212121212121212134343456536353465653|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 XX0 1q20 SL0 AN0 kin0 10M0 ak00 m10 8lb0 8L0 jd0 1qN0 WL0 1qN0","America/Argentina/Tucuman|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|012121212121212121212121212121212121212121343434345434323534343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wq0 Ra0 1wp0 TX0 g0p0 10M0 ako0 4N0 8BX0 uL0 1qN0 WL0","America/Argentina/Ushuaia|CMT ART ARST ART ARST WART|4g.M 40 30 30 20 40|0121212121212121212121212121212121212121213434343434343235343|-20UHH.c pKnH.c Mn0 1iN0 Tb0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 1C10 LX0 1C10 LX0 1C10 LX0 1C10 Mn0 MN0 2jz0 MN0 4lX0 u10 5Lb0 1pB0 Fnz0 u10 uL0 1vd0 SL0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 zvd0 Bz0 1tB0 TX0 1wp0 Rb0 1wp0 Rb0 1wp0 TX0 g0p0 10M0 ajA0 8p0 8zb0 uL0","America/Curacao|LMT ANT AST|4z.L 4u 40|012|-2kV7o.d 28KLS.d|15e4","America/Asuncion|AMT PYT PYT PYST|3O.E 40 30 30|012131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313|-1x589.k 1DKM9.k 3CL0 3Dd0 10L0 1pB0 10n0 1pB0 10n0 1pB0 1cL0 1dd0 1db0 1dd0 1cL0 1dd0 1cL0 1dd0 1cL0 1dd0 1db0 1dd0 1cL0 1dd0 1cL0 1dd0 1cL0 1dd0 1db0 1dd0 1cL0 1lB0 14n0 1dd0 1cL0 1fd0 WL0 1rd0 1aL0 1dB0 Xz0 1qp0 Xb0 1qN0 10L0 1rB0 TX0 1tB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 WN0 1qL0 11B0 1nX0 1ip0 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 TX0 1tB0 19X0 1a10 1fz0 1a10 1fz0 1cN0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0|28e5","America/Atikokan|CST CDT CWT CPT EST|60 50 50 50 50|0101234|-25TQ0 1in0 Rnb0 3je0 8x30 iw0|28e2","America/Bahia|LMT BRT BRST|2y.4 30 20|01212121212121212121212121212121212121212121212121212121212121|-2glxp.U HdLp.U 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 l5B0 Rb0|27e5","America/Bahia_Banderas|LMT MST CST PST MDT CDT|71 70 60 80 60 50|0121212131414141414141414141414141414152525252525252525252525252525252525252525252525252525252|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nW0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|84e3","America/Barbados|LMT BMT AST ADT|3W.t 3W.t 40 30|01232323232|-1Q0I1.v jsM0 1ODC1.v IL0 1ip0 17b0 1ip0 17b0 1ld0 13b0|28e4","America/Belem|LMT BRT BRST|3d.U 30 20|012121212121212121212121212121|-2glwK.4 HdKK.4 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0|20e5","America/Belize|LMT CST CHDT CDT|5Q.M 60 5u 50|01212121212121212121212121212121212121212121212121213131|-2kBu7.c fPA7.c Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1wou Rbu 1zcu Onu 1zcu Onu 1zcu Rbu 1wou Rbu 1f0Mu qn0 lxB0 mn0|57e3","America/Blanc-Sablon|AST ADT AWT APT|40 30 30 30|010230|-25TS0 1in0 UGp0 8x50 iu0|11e2","America/Boa_Vista|LMT AMT AMST|42.E 40 30|0121212121212121212121212121212121|-2glvV.k HdKV.k 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 smp0 WL0 1tB0 2L0|62e2","America/Bogota|BMT COT COST|4U.g 50 40|0121|-2eb73.I 38yo3.I 2en0|90e5","America/Boise|PST PDT MST MWT MPT MDT|80 70 70 60 60 60|0101023425252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-261q0 1nX0 11B0 1nX0 8C10 JCL0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 Dd0 1Kn0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|21e4","America/Cambridge_Bay|-00 MST MWT MPT MDDT MDT CST CDT EST|0 70 60 60 50 60 60 50 50|0123141515151515151515151515151515151515151515678651515151515151515151515151515151515151515151515151515151515151515151515151|-21Jc0 RO90 8x20 ix0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11A0 1nX0 2K0 WQ0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|15e2","America/Campo_Grande|LMT AMT AMST|3C.s 40 30|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwl.w HdLl.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 1C10 Lz0 1Ip0 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0|77e4","America/Cancun|LMT CST EST EDT CDT|5L.4 60 50 40 50|0123232341414141414141414141414141414141412|-1UQG0 2q2o0 yLB0 1lb0 14p0 1lb0 14p0 Lz0 xB0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 Dd0|63e4","America/Caracas|CMT VET VET|4r.E 4u 40|01212|-2kV7w.k 28KM2.k 1IwOu kqo0|29e5","America/Cayenne|LMT GFT GFT|3t.k 40 30|012|-2mrwu.E 2gWou.E|58e3","America/Panama|CMT EST|5j.A 50|01|-2uduE.o|15e5","America/Chicago|CST CDT EST CWT CPT|60 50 50 50 50|01010101010101010101010101010101010102010101010103401010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 1wp0 TX0 WN0 1qL0 1cN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 11B0 1Hz0 14p0 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 RB0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|92e5","America/Chihuahua|LMT MST CST CDT MDT|74.k 70 60 50 60|0121212323241414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 2zQN0 1lb0 14p0 1lb0 14q0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|81e4","America/Costa_Rica|SJMT CST CDT|5A.d 60 50|0121212121|-1Xd6n.L 2lu0n.L Db0 1Kp0 Db0 pRB0 15b0 1kp0 mL0|12e5","America/Creston|MST PST|70 80|010|-29DR0 43B0|53e2","America/Cuiaba|LMT AMT AMST|3I.k 40 30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwf.E HdLf.E 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 4a10 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0|54e4","America/Danmarkshavn|LMT WGT WGST GMT|1e.E 30 20 0|01212121212121212121212121212121213|-2a5WJ.k 2z5fJ.k 19U0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 DC0|8","America/Dawson|YST YDT YWT YPT YDDT PST PDT|90 80 80 80 70 80 70|0101023040565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-25TN0 1in0 1o10 13V0 Ser0 8x00 iz0 LCL0 1fA0 jrA0 fNd0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|13e2","America/Dawson_Creek|PST PDT PWT PPT MST|80 70 70 70 70|0102301010101010101010101010101010101010101010101010101014|-25TO0 1in0 UGp0 8x10 iy0 3NB0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 ML0|12e3","America/Denver|MST MDT MWT MPT|70 60 60 60|01010101023010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261r0 1nX0 11B0 1nX0 11B0 1qL0 WN0 mn0 Ord0 8x20 ix0 LCN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|26e5","America/Detroit|LMT CST EST EWT EPT EDT|5w.b 60 50 40 40 40|01234252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-2Cgir.N peqr.N 156L0 8x40 iv0 6fd0 11z0 Jy10 SL0 dnB0 1cL0 s10 1Vz0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|37e5","America/Edmonton|LMT MST MDT MWT MPT|7x.Q 70 60 60 60|01212121212121341212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2yd4q.8 shdq.8 1in0 17d0 hz0 2dB0 1fz0 1a10 11z0 1qN0 WL0 1qN0 11z0 IGN0 8x20 ix0 3NB0 11z0 LFB0 1cL0 3Cp0 1cL0 66N0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|10e5","America/Eirunepe|LMT ACT ACST AMT|4D.s 50 40 40|0121212121212121212121212121212131|-2glvk.w HdLk.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 dPB0 On0 yTd0 d5X0|31e3","America/El_Salvador|LMT CST CDT|5U.M 60 50|012121|-1XiG3.c 2Fvc3.c WL0 1qN0 WL0|11e5","America/Tijuana|LMT MST PST PDT PWT PPT|7M.4 70 80 70 70 70|012123245232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UQE0 4PX0 8mM0 8lc0 SN0 1cL0 pHB0 83r0 zI0 5O10 1Rz0 cOP0 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 BUp0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|20e5","America/Fort_Nelson|PST PDT PWT PPT MST|80 70 70 70 70|01023010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010104|-25TO0 1in0 UGp0 8x10 iy0 3NB0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0|39e2","America/Fort_Wayne|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|010101023010101010101010101040454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 QI10 Db0 RB0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 5Tz0 1o10 qLb0 1cL0 1cN0 1cL0 1qhd0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Fortaleza|LMT BRT BRST|2y 30 20|0121212121212121212121212121212121212121|-2glxq HdLq 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 5z0 2mN0 On0|34e5","America/Glace_Bay|LMT AST ADT AWT APT|3X.M 40 30 30 30|012134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsI0.c CwO0.c 1in0 UGp0 8x50 iu0 iq10 11z0 Jg10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|19e3","America/Godthab|LMT WGT WGST|3q.U 30 20|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a5Ux.4 2z5dx.4 19U0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|17e3","America/Goose_Bay|NST NDT NST NDT NWT NPT AST ADT ADDT|3u.Q 2u.Q 3u 2u 2u 2u 40 30 20|010232323232323245232323232323232323232323232323232323232326767676767676767676767676767676767676767676768676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-25TSt.8 1in0 DXb0 2HbX.8 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 WL0 1qN0 WL0 1qN0 7UHu itu 1tB0 WL0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1tB0 WL0 1ld0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 S10 g0u 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14n1 1lb0 14p0 1nW0 11C0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|76e2","America/Grand_Turk|KMT EST EDT AST|57.b 50 40 40|0121212121212121212121212121212121212121212121212121212121212121212121212123|-2l1uQ.N 2HHBQ.N 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|37e2","America/Guatemala|LMT CST CDT|62.4 60 50|0121212121|-24KhV.U 2efXV.U An0 mtd0 Nz0 ifB0 17b0 zDB0 11z0|13e5","America/Guayaquil|QMT ECT|5e 50|01|-1yVSK|27e5","America/Guyana|LMT GBGT GYT GYT GYT|3Q.E 3J 3J 30 40|01234|-2dvU7.k 24JzQ.k mlc0 Bxbf|80e4","America/Halifax|LMT AST ADT AWT APT|4e.o 40 30 30 30|0121212121212121212121212121212121212121212121212134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsHJ.A xzzJ.A 1db0 3I30 1in0 3HX0 IL0 1E10 ML0 1yN0 Pb0 1Bd0 Mn0 1Bd0 Rz0 1w10 Xb0 1w10 LX0 1w10 Xb0 1w10 Lz0 1C10 Jz0 1E10 OL0 1yN0 Un0 1qp0 Xb0 1qp0 11X0 1w10 Lz0 1HB0 LX0 1C10 FX0 1w10 Xb0 1qp0 Xb0 1BB0 LX0 1td0 Xb0 1qp0 Xb0 Rf0 8x50 iu0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 3Qp0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 3Qp0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 6i10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|39e4","America/Havana|HMT CST CDT|5t.A 50 40|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1Meuu.o 72zu.o ML0 sld0 An0 1Nd0 Db0 1Nd0 An0 6Ep0 An0 1Nd0 An0 JDd0 Mn0 1Ap0 On0 1fd0 11X0 1qN0 WL0 1wp0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 14n0 1ld0 14L0 1kN0 15b0 1kp0 1cL0 1cN0 1fz0 1a10 1fz0 1fB0 11z0 14p0 1nX0 11B0 1nX0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 1a10 1in0 1a10 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 17c0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 11A0 6i00 Rc0 1wo0 U00 1tA0 Rc0 1wo0 U00 1wo0 U00 1zc0 U00 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0|21e5","America/Hermosillo|LMT MST CST PST MDT|7n.Q 70 60 80 60|0121212131414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0|64e4","America/Indiana/Knox|CST CDT CWT CPT EST|60 50 50 50 50|0101023010101010101010101010101010101040101010101010101010101010101010101010101010101010141010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 3NB0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 11z0 1o10 11z0 1o10 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 3Cn0 8wp0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 z8o0 1o00 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Marengo|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101023010101010101010104545454545414545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 dyN0 11z0 6fd0 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 jrz0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1VA0 LA0 1BX0 1e6p0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Petersburg|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010104010101010101010101010141014545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 njX0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 3Fb0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 19co0 1o00 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Tell_City|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010454541010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 g0p0 11z0 1o10 11z0 1qL0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 caL0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Vevay|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|010102304545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 kPB0 Awn0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1lnd0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Vincennes|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010454541014545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 g0p0 11z0 1o10 11z0 1qL0 WN0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 caL0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Indiana/Winamac|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|01010230101010101010101010101010101010454541054545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 jrz0 1cL0 1cN0 1cL0 1qhd0 1o00 Rd0 1za0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Inuvik|-00 PST PDDT MST MDT|0 80 60 70 60|0121343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-FnA0 tWU0 1fA0 wPe0 2pz0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|35e2","America/Iqaluit|-00 EWT EPT EST EDDT EDT CST CDT|0 40 40 50 30 40 60 50|01234353535353535353535353535353535353535353567353535353535353535353535353535353535353535353535353535353535353535353535353|-16K00 7nX0 iv0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11C0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|67e2","America/Jamaica|KMT EST EDT|57.b 50 40|0121212121212121212121|-2l1uQ.N 2uM1Q.N 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0|94e4","America/Juneau|PST PWT PPT PDT YDT YST AKST AKDT|80 70 70 70 80 90 90 80|01203030303030303030303030403030356767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cM0 1cM0 1cL0 1cN0 1fz0 1a10 1fz0 co0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|33e3","America/Kentucky/Louisville|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101010102301010101010101010101010101454545454545414545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 3Fd0 Nb0 LPd0 11z0 RB0 8x30 iw0 Bb0 10N0 2bB0 8in0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 xz0 gso0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1VA0 LA0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Kentucky/Monticello|CST CDT CWT CPT EST EDT|60 50 50 50 50 40|0101023010101010101010101010101010101010101010101010101010101010101010101454545454545454545454545454545454545454545454545454545454545454545454545454|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 SWp0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/La_Paz|CMT BOST BOT|4w.A 3w.A 40|012|-1x37r.o 13b0|19e5","America/Lima|LMT PET PEST|58.A 50 40|0121212121212121|-2tyGP.o 1bDzP.o zX0 1aN0 1cL0 1cN0 1cL0 1PrB0 zX0 1O10 zX0 6Gp0 zX0 98p0 zX0|11e6","America/Los_Angeles|PST PDT PWT PPT|80 70 70 70|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261q0 1nX0 11B0 1nX0 SgN0 8x10 iy0 5Wp0 1Vb0 3dB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|15e6","America/Maceio|LMT BRT BRST|2m.Q 30 20|012121212121212121212121212121212121212121|-2glxB.8 HdLB.8 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 dMN0 Lz0 8Q10 WL0 1tB0 5z0 2mN0 On0|93e4","America/Managua|MMT CST EST CDT|5J.c 60 50 50|0121313121213131|-1quie.M 1yAMe.M 4mn0 9Up0 Dz0 1K10 Dz0 s3F0 1KH0 DB0 9In0 k8p0 19X0 1o30 11y0|22e5","America/Manaus|LMT AMT AMST|40.4 40 30|01212121212121212121212121212121|-2glvX.U HdKX.U 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 dPB0 On0|19e5","America/Martinique|FFMT AST ADT|44.k 40 30|0121|-2mPTT.E 2LPbT.E 19X0|39e4","America/Matamoros|LMT CST CDT|6E 60 50|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1UQG0 2FjC0 1nX0 i6p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|45e4","America/Mazatlan|LMT MST CST PST MDT|75.E 70 60 80 60|0121212131414141414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 otX0 gmN0 P2N0 13Vd0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|44e4","America/Menominee|CST CDT CWT CPT EST|60 50 50 50 50|01010230101041010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 1o10 11z0 LCN0 1fz0 6410 9Jb0 1cM0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|85e2","America/Merida|LMT CST EST CDT|5W.s 60 50 50|0121313131313131313131313131313131313131313131313131313131313131313131313131313131313131|-1UQG0 2q2o0 2hz0 wu30 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|11e5","America/Metlakatla|PST PWT PPT PDT AKST AKDT|80 70 70 70 90 80|0120303030303030303030303030303030454545454545454545454545454545454545454545454|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1hU10 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|14e2","America/Mexico_City|LMT MST CST CDT CWT|6A.A 70 60 50 50|012121232324232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 gEn0 TX0 3xd0 Jb0 6zB0 SL0 e5d0 17b0 1Pff0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|20e6","America/Miquelon|LMT AST PMST PMDT|3I.E 40 30 20|012323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-2mKkf.k 2LTAf.k gQ10 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|61e2","America/Moncton|EST AST ADT AWT APT|50 40 30 30 30|012121212121212121212134121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2IsH0 CwN0 1in0 zAo0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1Nd0 An0 1K10 Lz0 1zB0 NX0 1u10 Wn0 S20 8x50 iu0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 3Cp0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14n1 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 ReX 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|64e3","America/Monterrey|LMT CST CDT|6F.g 60 50|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1UQG0 2FjC0 1nX0 i6p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|41e5","America/Montevideo|MMT UYT UYHST UYST UYT UYHST|3I.I 3u 30 20 30 2u|012121212121212121212121213434343434345454543453434343434343434343434343434343434343434|-20UIf.g 8jzJ.g 1cLu 1dcu 1cLu 1dcu 1cLu ircu 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 1qMu WLu 1qMu WLu 1qMu 11zu 1o0u 11zu NAu 11bu 2iMu zWu Dq10 19X0 pd0 jz0 cm10 19X0 1fB0 1on0 11d0 1oL0 1nB0 1fzu 1aou 1fzu 1aou 1fzu 3nAu Jb0 3MN0 1SLu 4jzu 2PB0 Lb0 3Dd0 1pb0 ixd0 An0 1MN0 An0 1wp0 On0 1wp0 Rb0 1zd0 On0 1wp0 Rb0 s8p0 1fB0 1ip0 11z0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 14n0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 11z0|17e5","America/Toronto|EST EDT EWT EPT|50 40 40 40|01010101010101010101010101010101010101010101012301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TR0 1in0 11Wu 1nzu 1fD0 WJ0 1wr0 Nb0 1Ap0 On0 1zd0 On0 1wp0 TX0 1tB0 TX0 1tB0 TX0 1tB0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 4kM0 8x40 iv0 1o10 11z0 1nX0 11z0 1o10 11z0 1o10 1qL0 11D0 1nX0 11B0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|65e5","America/Nassau|LMT EST EDT|59.u 50 40|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2kNuO.u 26XdO.u 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|24e4","America/New_York|EST EDT EWT EPT|50 40 40 40|01010101010101010101010101010101010101010101010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261t0 1nX0 11B0 1nX0 11B0 1qL0 1a10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 RB0 8x40 iv0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|21e6","America/Nipigon|EST EDT EWT EPT|50 40 40 40|010123010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TR0 1in0 Rnb0 3je0 8x40 iv0 19yN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|16e2","America/Nome|NST NWT NPT BST BDT YST AKST AKDT|b0 a0 a0 b0 a0 90 90 80|012034343434343434343434343434343456767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676|-17SX0 8wW0 iB0 Qlb0 52O0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cl0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|38e2","America/Noronha|LMT FNT FNST|29.E 20 10|0121212121212121212121212121212121212121|-2glxO.k HdKO.k 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 2L0 2pB0 On0|30e2","America/North_Dakota/Beulah|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101014545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Oo0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/North_Dakota/Center|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101014545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14o0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/North_Dakota/New_Salem|MST MDT MWT MPT CST CDT|70 60 60 60 60 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101454545454545454545454545454545454545454545454545454545454545454545454|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14o0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","America/Ojinaga|LMT MST CST CDT MDT|6V.E 70 60 50 60|0121212323241414141414141414141414141414141414141414141414141414141414141414141414141414141|-1UQF0 deL0 8lc0 17c0 10M0 1dd0 2zQN0 1lb0 14p0 1lb0 14q0 1lb0 14p0 1nX0 11B0 1nX0 1fB0 WL0 1fB0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 U10 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|23e3","America/Pangnirtung|-00 AST AWT APT ADDT ADT EDT EST CST CDT|0 40 30 30 20 30 40 50 60 50|012314151515151515151515151515151515167676767689767676767676767676767676767676767676767676767676767676767676767676767676767|-1XiM0 PnG0 8x50 iu0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1o00 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11C0 1nX0 11A0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|14e2","America/Paramaribo|LMT PMT PMT NEGT SRT SRT|3E.E 3E.Q 3E.A 3u 3u 30|012345|-2nDUj.k Wqo0.c qanX.I 1dmLN.o lzc0|24e4","America/Phoenix|MST MDT MWT|70 60 60|01010202010|-261r0 1nX0 11B0 1nX0 SgN0 4Al1 Ap0 1db0 SWqX 1cL0|42e5","America/Port-au-Prince|PPMT EST EDT|4N 50 40|01212121212121212121212121212121212121212121|-28RHb 2FnMb 19X0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14q0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 i6n0 1nX0 11B0 1nX0 d430 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|23e5","America/Rio_Branco|LMT ACT ACST AMT|4v.c 50 40 40|01212121212121212121212121212131|-2glvs.M HdLs.M 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 NBd0 d5X0|31e4","America/Porto_Velho|LMT AMT AMST|4f.A 40 30|012121212121212121212121212121|-2glvI.o HdKI.o 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0|37e4","America/Puerto_Rico|AST AWT APT|40 30 30|0120|-17lU0 7XT0 iu0|24e5","America/Rainy_River|CST CDT CWT CPT|60 50 50 50|010123010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TQ0 1in0 Rnb0 3je0 8x30 iw0 19yN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|842","America/Rankin_Inlet|-00 CST CDDT CDT EST|0 60 40 50 50|012131313131313131313131313131313131313131313431313131313131313131313131313131313131313131313131313131313131313131313131|-vDc0 keu0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|26e2","America/Recife|LMT BRT BRST|2j.A 30 20|0121212121212121212121212121212121212121|-2glxE.o HdLE.o 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 nsp0 WL0 1tB0 2L0 2pB0 On0|33e5","America/Regina|LMT MST MDT MWT MPT CST|6W.A 70 60 60 60 60|012121212121212121212121341212121212121212121212121215|-2AD51.o uHe1.o 1in0 s2L0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 66N0 1cL0 1cN0 19X0 1fB0 1cL0 1fB0 1cL0 1cN0 1cL0 M30 8x20 ix0 1ip0 1cL0 1ip0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 3NB0 1cL0 1cN0|19e4","America/Resolute|-00 CST CDDT CDT EST|0 60 40 50 50|012131313131313131313131313131313131313131313431313131313431313131313131313131313131313131313131313131313131313131313131|-SnA0 GWS0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|229","America/Santarem|LMT AMT AMST BRT|3C.M 40 30 30|0121212121212121212121212121213|-2glwl.c HdLl.c 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 qe10 xb0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 NBd0|21e4","America/Santiago|SMT CLT CLT CLST CLST|4G.K 50 40 40 30|010203131313131212421242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424|-2q2jh.e fJAh.e 5knG.K 1Vzh.e jRAG.K 1pbh.e 11d0 1oL0 11d0 1oL0 11d0 1oL0 11d0 1pb0 11d0 nHX0 op0 9Bz0 jb0 1oN0 ko0 Qeo0 WL0 1zd0 On0 1ip0 11z0 1o10 11z0 1qN0 WL0 1ld0 14n0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1o10 11z0 1qN0 WL0 1fB0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0|62e5","America/Santo_Domingo|SDMT EST EDT EHDT AST|4E 50 40 4u 40|01213131313131414|-1ttjk 1lJMk Mn0 6sp0 Lbu 1Cou yLu 1RAu wLu 1QMu xzu 1Q0u xXu 1PAu 13jB0 e00|29e5","America/Sao_Paulo|LMT BRT BRST|36.s 30 20|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-2glwR.w HdKR.w 1cc0 1e10 1bX0 Ezd0 So0 1vA0 Mn0 1BB0 ML0 1BB0 zX0 pTd0 PX0 2ep0 nz0 1C10 zX0 1C10 LX0 1C10 Mn0 H210 Rb0 1tB0 IL0 1Fd0 FX0 1EN0 FX0 1HB0 Lz0 1EN0 Lz0 1C10 IL0 1HB0 Db0 1HB0 On0 1zd0 On0 1zd0 Lz0 1zd0 Rb0 1wN0 Wn0 1tB0 Rb0 1tB0 WL0 1tB0 Rb0 1zd0 On0 1HB0 FX0 1C10 Lz0 1Ip0 HX0 1zd0 On0 1HB0 IL0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10 Lz0 1C10 On0 1zd0 Rb0 1wp0 On0 1C10 Lz0 1C10 On0 1zd0|20e6","America/Scoresbysund|LMT CGT CGST EGST EGT|1r.Q 20 10 0 10|0121343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-2a5Ww.8 2z5ew.8 1a00 1cK0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|452","America/Sitka|PST PWT PPT PDT YST AKST AKDT|80 70 70 70 90 90 80|01203030303030303030303030303030345656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-17T20 8x10 iy0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 co0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|90e2","America/St_Johns|NST NDT NST NDT NWT NPT NDDT|3u.Q 2u.Q 3u 2u 2u 2u 1u|01010101010101010101010101010101010102323232323232324523232323232323232323232323232323232323232323232323232323232323232323232323232323232326232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-28oit.8 14L0 1nB0 1in0 1gm0 Dz0 1JB0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1fB0 1cL0 1fB0 19X0 1fB0 19X0 10O0 eKX.8 19X0 1iq0 WL0 1qN0 WL0 1qN0 WL0 1tB0 TX0 1tB0 WL0 1qN0 WL0 1qN0 7UHu itu 1tB0 WL0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1tB0 WL0 1ld0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14n1 1lb0 14p0 1nW0 11C0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|11e4","America/Swift_Current|LMT MST MDT MWT MPT CST|7b.k 70 60 60 60 60|012134121212121212121215|-2AD4M.E uHdM.E 1in0 UGp0 8x20 ix0 1o10 17b0 1ip0 11z0 1o10 11z0 1o10 11z0 isN0 1cL0 3Cp0 1cL0 1cN0 11z0 1qN0 WL0 pMp0|16e3","America/Tegucigalpa|LMT CST CDT|5M.Q 60 50|01212121|-1WGGb.8 2ETcb.8 WL0 1qN0 WL0 GRd0 AL0|11e5","America/Thule|LMT AST ADT|4z.8 40 30|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a5To.Q 31NBo.Q 1cL0 1cN0 1cL0 1fB0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|656","America/Thunder_Bay|CST EST EWT EPT EDT|60 50 40 40 40|0123141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141|-2q5S0 1iaN0 8x40 iv0 XNB0 1cL0 1cN0 1fz0 1cN0 1cL0 3Cp0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|11e4","America/Vancouver|PST PDT PWT PPT|80 70 70 70|0102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-25TO0 1in0 UGp0 8x10 iy0 1o10 17b0 1ip0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|23e5","America/Whitehorse|YST YDT YWT YPT YDDT PST PDT|90 80 80 80 70 80 70|0101023040565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565|-25TN0 1in0 1o10 13V0 Ser0 8x00 iz0 LCL0 1fA0 3NA0 vrd0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|23e3","America/Winnipeg|CST CDT CWT CPT|60 50 50 50|010101023010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aIi0 WL0 3ND0 1in0 Jap0 Rb0 aCN0 8x30 iw0 1tB0 11z0 1ip0 11z0 1o10 11z0 1o10 11z0 1rd0 10L0 1op0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 1cL0 1cN0 11z0 6i10 WL0 6i10 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1o00 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1o00 11A0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|66e4","America/Yakutat|YST YWT YPT YDT AKST AKDT|90 80 80 80 90 80|01203030303030303030303030303030304545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-17T10 8x00 iz0 Vo10 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 cn0 10q0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|642","America/Yellowknife|-00 MST MWT MPT MDDT MDT|0 70 60 60 50 60|012314151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151|-1pdA0 hix0 8x20 ix0 LCL0 1fA0 zgO0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|19e3","Antarctica/Casey|-00 AWST CAST|0 -80 -b0|012121|-2q00 1DjS0 T90 40P0 KL0|10","Antarctica/Davis|-00 DAVT DAVT|0 -70 -50|01012121|-vyo0 iXt0 alj0 1D7v0 VB0 3Wn0 KN0|70","Antarctica/DumontDUrville|-00 PMT DDUT|0 -a0 -a0|0102|-U0o0 cfq0 bFm0|80","Antarctica/Macquarie|AEST AEDT -00 MIST|-a0 -b0 0 -b0|0102010101010101010101010101010101010101010101010101010101010101010101010101010101010101013|-29E80 19X0 4SL0 1ayy0 Lvs0 1cM0 1o00 Rc0 1wo0 Rc0 1wo0 U00 1wo0 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0|1","Antarctica/Mawson|-00 MAWT MAWT|0 -60 -50|012|-CEo0 2fyk0|60","Pacific/Auckland|NZMT NZST NZST NZDT|-bu -cu -c0 -d0|01020202020202020202020202023232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323|-1GCVu Lz0 1tB0 11zu 1o0u 11zu 1o0u 11zu 1o0u 14nu 1lcu 14nu 1lcu 1lbu 11Au 1nXu 11Au 1nXu 11Au 1nXu 11Au 1nXu 11Au 1qLu WMu 1qLu 11Au 1n1bu IM0 1C00 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1qM0 14o0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1io0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|14e5","Antarctica/Palmer|-00 ARST ART ART ARST CLT CLST|0 30 40 30 20 40 30|0121212121234356565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656|-cao0 nD0 1vd0 SL0 1vd0 17z0 1cN0 1fz0 1cN0 1cL0 1cN0 asn0 Db0 jsN0 14N0 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1o10 11z0 1qN0 WL0 1fB0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0|40","Antarctica/Rothera|-00 ROTT|0 30|01|gOo0|130","Antarctica/Syowa|-00 SYOT|0 -30|01|-vs00|20","Antarctica/Troll|-00 UTC CEST|0 0 -20|01212121212121212121212121212121212121212121212121212121212121212121|1puo0 hd0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|40","Antarctica/Vostok|-00 VOST|0 -60|01|-tjA0|25","Europe/Oslo|CET CEST|-10 -20|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2awM0 Qm0 W6o0 5pf0 WM0 1fA0 1cM0 1cM0 1cM0 1cM0 wJc0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1qM0 WM0 zpc0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|62e4","Asia/Riyadh|LMT AST|-36.Q -30|01|-TvD6.Q|57e5","Asia/Almaty|LMT +05 +06 +07|-57.M -50 -60 -70|012323232323232323232321232323232323232323232323232|-1Pc57.M eUo7.M 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0|15e5","Asia/Amman|LMT EET EEST|-2n.I -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1yW2n.I 1HiMn.I KL0 1oN0 11b0 1oN0 11b0 1pd0 1dz0 1cp0 11b0 1op0 11b0 fO10 1db0 1e10 1cL0 1cN0 1cL0 1cN0 1fz0 1pd0 10n0 1ld0 14n0 1hB0 15b0 1ip0 19X0 1cN0 1cL0 1cN0 17b0 1ld0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1So0 y00 1fc0 1dc0 1co0 1dc0 1cM0 1cM0 1cM0 1o00 11A0 1lc0 17c0 1cM0 1cM0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 4bX0 Dd0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|25e5","Asia/Anadyr|LMT ANAT ANAT ANAST ANAST ANAST ANAT|-bN.U -c0 -d0 -e0 -d0 -c0 -b0|01232414141414141414141561414141414141414141414141414141414141561|-1PcbN.U eUnN.U 23CL0 1db0 1cN0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0|13e3","Asia/Aqtau|LMT +04 +05 +06|-3l.4 -40 -50 -60|012323232323232323232123232312121212121212121212|-1Pc3l.4 eUnl.4 24PX0 2pX0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0|15e4","Asia/Aqtobe|LMT +04 +05 +06|-3M.E -40 -50 -60|0123232323232323232321232323232323232323232323232|-1Pc3M.E eUnM.E 23CL0 3Db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0|27e4","Asia/Ashgabat|LMT ASHT ASHT ASHST ASHST TMT TMT|-3R.w -40 -50 -60 -50 -40 -50|012323232323232323232324156|-1Pc3R.w eUnR.w 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 ba0 xC0|41e4","Asia/Baghdad|BMT AST ADT|-2V.A -30 -40|012121212121212121212121212121212121212121212121212121|-26BeV.A 2ACnV.A 11b0 1cp0 1dz0 1dd0 1db0 1cN0 1cp0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1de0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0 1dc0 1dc0 1cM0 1dc0 1cM0 1dc0 1cM0 1dc0|66e5","Asia/Qatar|LMT GST AST|-3q.8 -40 -30|012|-21Jfq.8 27BXq.8|96e4","Asia/Baku|LMT BAKT BAKT BAKST BAKST AZST AZT AZT AZST|-3j.o -30 -40 -50 -40 -40 -30 -40 -50|01232323232323232323232456578787878787878787878787878787878787878787|-1Pc3j.o 1jUoj.o WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 10K0 c30 1cM0 1cM0 8wq0 1o00 11z0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00|27e5","Asia/Bangkok|BMT ICT|-6G.4 -70|01|-218SG.4|15e6","Asia/Barnaul|LMT +06 +07 +08|-5z -60 -70 -80|0123232323232323232323212323232321212121212121212121212121212121212|-21S5z pCnz 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 p90 LE0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3rd0","Asia/Beirut|EET EEST|-20 -30|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-21aq0 1on0 1410 1db0 19B0 1in0 1ip0 WL0 1lQp0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 q6N0 En0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1op0 11b0 dA10 17b0 1iN0 17b0 1iN0 17b0 1iN0 17b0 1vB0 SL0 1mp0 13z0 1iN0 17b0 1iN0 17b0 1jd0 12n0 1a10 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0|22e5","Asia/Bishkek|LMT FRUT FRUT FRUST FRUST KGT KGST KGT|-4W.o -50 -60 -70 -60 -50 -60 -60|01232323232323232323232456565656565656565656565656567|-1Pc4W.o eUnW.o 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11c0 1tX0 17b0 1ip0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1cPu 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 T8u|87e4","Asia/Brunei|LMT BNT BNT|-7D.E -7u -80|012|-1KITD.E gDc9.E|42e4","Asia/Kolkata|HMT BURT IST IST|-5R.k -6u -5u -6u|01232|-18LFR.k 1unn.k HB0 7zX0|15e6","Asia/Chita|LMT YAKT YAKT YAKST YAKST YAKT IRKT|-7x.Q -80 -90 -a0 -90 -a0 -80|0123232323232323232323241232323232323232323232323232323232323232562|-21Q7x.Q pAnx.Q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3re0|33e4","Asia/Choibalsan|LMT ULAT ULAT CHOST CHOT CHOT CHOST|-7C -70 -80 -a0 -90 -80 -90|0123434343434343434343434343434343434343434343456565656565656565656565656565656565656565656565|-2APHC 2UkoC cKn0 1da0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 3Db0 h1f0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|38e3","Asia/Shanghai|CST CDT|-80 -90|01010101010101010|-1c1I0 LX0 16p0 1jz0 1Myp0 Rb0 1o10 11z0 1o10 11z0 1qN0 11z0 1o10 11z0 1o10 11z0|23e6","Asia/Colombo|MMT IST IHST IST LKT LKT|-5j.w -5u -60 -6u -6u -60|01231451|-2zOtj.w 1rFbN.w 1zzu 7Apu 23dz0 11zu n3cu|22e5","Asia/Dhaka|HMT BURT IST DACT BDT BDST|-5R.k -6u -5u -60 -60 -70|01213454|-18LFR.k 1unn.k HB0 m6n0 LqMu 1x6n0 1i00|16e6","Asia/Damascus|LMT EET EEST|-2p.c -20 -30|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-21Jep.c Hep.c 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1xRB0 11X0 1oN0 10L0 1pB0 11b0 1oN0 10L0 1mp0 13X0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 1pd0 11b0 1oN0 Nb0 1AN0 Nb0 bcp0 19X0 1gp0 19X0 3ld0 1xX0 Vd0 1Bz0 Sp0 1vX0 10p0 1dz0 1cN0 1cL0 1db0 1db0 1g10 1an0 1ap0 1db0 1fd0 1db0 1cN0 1db0 1dd0 1db0 1cp0 1dz0 1c10 1dX0 1cN0 1db0 1dd0 1db0 1cN0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1db0 1cN0 1db0 1cN0 19z0 1fB0 1qL0 11B0 1on0 Wp0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0|26e5","Asia/Dili|LMT TLT JST TLT WITA|-8m.k -80 -90 -90 -80|012343|-2le8m.k 1dnXm.k 8HA0 1ew00 Xld0|19e4","Asia/Dubai|LMT GST|-3F.c -40|01|-21JfF.c|39e5","Asia/Dushanbe|LMT DUST DUST DUSST DUSST TJT|-4z.c -50 -60 -70 -60 -50|0123232323232323232323245|-1Pc4z.c eUnz.c 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 14N0|76e4","Asia/Gaza|EET EET EEST IST IDT|-20 -30 -30 -20 -30|010101010102020202020202020202023434343434343434343434343430202020202020202020202020202020202020202020202020202020202020202020202020202020202020|-1c2q0 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 pBd0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 dW0 hfB0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 M10 C00 17c0 1io0 17c0 1io0 17c0 1o00 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 17c0 1io0 18N0 1bz0 19z0 1gp0 1610 1iL0 11z0 1o10 14o0 1lA1 SKX 1xd1 MKX 1AN0 1a00 1fA0 1cL0 1cN0 1nX0 1210 1nz0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0|18e5","Asia/Hebron|EET EET EEST IST IDT|-20 -30 -30 -20 -30|01010101010202020202020202020202343434343434343434343434343020202020202020202020202020202020202020202020202020202020202020202020202020202020202020|-1c2q0 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 pBd0 Vz0 1oN0 11b0 1oO0 10N0 1pz0 10N0 1pb0 10N0 1pb0 10N0 1pb0 10N0 1pz0 10N0 1pb0 10N0 1pb0 11d0 1oL0 dW0 hfB0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 M10 C00 17c0 1io0 17c0 1io0 17c0 1o00 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 17c0 1io0 18N0 1bz0 19z0 1gp0 1610 1iL0 12L0 1mN0 14o0 1lc0 Tb0 1xd1 MKX bB0 cn0 1cN0 1a00 1fA0 1cL0 1cN0 1nX0 1210 1nz0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1ny0 1220 1qm0 1220 1ny0 1220 1ny0 1220 1ny0|25e4","Asia/Ho_Chi_Minh|LMT PLMT ICT IDT JST|-76.E -76.u -70 -80 -90|0123423232|-2yC76.E bK00.a 1h7b6.u 5lz0 18o0 3Oq0 k5b0 aW00 BAM0|90e5","Asia/Hong_Kong|LMT HKT HKST JST|-7A.G -80 -90 -90|0121312121212121212121212121212121212121212121212121212121212121212121|-2CFHA.G 1sEP6.G 1cL0 ylu 93X0 1qQu 1tX0 Rd0 1In0 NB0 1cL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1kL0 14N0 1nX0 U10 1tz0 U10 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1wn0 Rd0 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 17d0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 s10 1Vz0 1cN0 1cL0 1cN0 1cL0 6fd0 14n0|73e5","Asia/Hovd|LMT HOVT HOVT HOVST|-66.A -60 -70 -80|012323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-2APG6.A 2Uko6.A cKn0 1db0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 kEp0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|81e3","Asia/Irkutsk|IMT IRKT IRKT IRKST IRKST IRKT|-6V.5 -70 -80 -90 -80 -90|012323232323232323232324123232323232323232323232323232323232323252|-21zGV.5 pjXV.5 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|60e4","Europe/Istanbul|IMT EET EEST TRST TRT|-1U.U -20 -30 -40 -30|012121212121212121212121212121212121212121212121212121234343434342121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ogNU.U dzzU.U 11b0 8tB0 1on0 1410 1db0 19B0 1in0 3Rd0 Un0 1oN0 11b0 zSp0 CL0 mN0 1Vz0 1gN0 1pz0 5Rd0 1fz0 1yp0 ML0 1kp0 17b0 1ip0 17b0 1fB0 19X0 1jB0 18L0 1ip0 17z0 qdd0 xX0 3S10 Tz0 dA10 11z0 1o10 11z0 1qN0 11z0 1ze0 11B0 WM0 1qO0 WI0 1nX0 1rB0 10L0 11B0 1in0 17d0 1in0 2pX0 19E0 1fU0 16Q0 1iI0 16Q0 1iI0 1Vd0 pb0 3Kp0 14o0 1df0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WO0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 Xc0 1qo0 WM0 1qM0 11A0 1o00 1200 1nA0 11A0 1tA0 U00 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|13e6","Asia/Jakarta|BMT JAVT WIB JST WIB WIB|-77.c -7k -7u -90 -80 -70|01232425|-1Q0Tk luM0 mPzO 8vWu 6kpu 4PXu xhcu|31e6","Asia/Jayapura|LMT WIT ACST|-9m.M -90 -9u|0121|-1uu9m.M sMMm.M L4nu|26e4","Asia/Jerusalem|JMT IST IDT IDDT|-2k.E -20 -30 -40|01212121212132121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-26Bek.E SyMk.E 5Rb0 10r0 1px0 10N0 1pz0 16p0 1jB0 16p0 1jx0 3LB0 Em0 or0 1cn0 1dB0 16n0 10O0 1ja0 1tC0 14o0 1cM0 1a00 11A0 1Na0 An0 1MP0 AJ0 1Kp0 LC0 1oo0 Wl0 EQN0 Db0 1fB0 Rb0 npB0 11z0 1C10 IL0 1s10 10n0 1o10 WL0 1zd0 On0 1ld0 11z0 1o10 14n0 1o10 14n0 1nd0 12n0 1nd0 Xz0 1q10 12n0 1hB0 1dX0 1ep0 1aL0 1eN0 17X0 1nf0 11z0 1tB0 19W0 1e10 17b0 1ep0 1gL0 18N0 1fz0 1eN0 17b0 1gq0 1gn0 19d0 1dz0 1c10 17X0 1hB0 1gn0 19d0 1dz0 1c10 17X0 1kp0 1dz0 1c10 1aL0 1eN0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0|81e4","Asia/Kabul|AFT AFT|-40 -4u|01|-10Qs0|46e5","Asia/Kamchatka|LMT PETT PETT PETST PETST|-ay.A -b0 -c0 -d0 -c0|01232323232323232323232412323232323232323232323232323232323232412|-1SLKy.A ivXy.A 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0|18e4","Asia/Karachi|LMT IST IST KART PKT PKST|-4s.c -5u -6u -50 -50 -60|012134545454|-2xoss.c 1qOKW.c 7zX0 eup0 LqMu 1fy00 1cL0 dK10 11b0 1610 1jX0|24e6","Asia/Urumqi|LMT XJT|-5O.k -60|01|-1GgtO.k|32e5","Asia/Kathmandu|LMT IST NPT|-5F.g -5u -5J|012|-21JhF.g 2EGMb.g|12e5","Asia/Khandyga|LMT YAKT YAKT YAKST YAKST VLAT VLAST VLAT YAKT|-92.d -80 -90 -a0 -90 -a0 -b0 -b0 -a0|01232323232323232323232412323232323232323232323232565656565656565782|-21Q92.d pAp2.d 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 qK0 yN0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 17V0 7zD0|66e2","Asia/Krasnoyarsk|LMT KRAT KRAT KRAST KRAST KRAT|-6b.q -60 -70 -80 -70 -80|012323232323232323232324123232323232323232323232323232323232323252|-21Hib.q prAb.q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|10e5","Asia/Kuala_Lumpur|SMT MALT MALST MALT MALT JST MYT|-6T.p -70 -7k -7k -7u -90 -80|01234546|-2Bg6T.p 17anT.p 7hXE dM00 17bO 8Fyu 1so1u|71e5","Asia/Kuching|LMT BORT BORT BORTST JST MYT|-7l.k -7u -80 -8k -90 -80|01232323232323232425|-1KITl.k gDbP.k 6ynu AnE 1O0k AnE 1NAk AnE 1NAk AnE 1NAk AnE 1O0k AnE 1NAk AnE pAk 8Fz0 1so10|13e4","Asia/Macau|LMT MOT MOST CST|-7y.k -80 -90 -80|0121212121212121212121212121212121212121213|-2le7y.k 1XO34.k 1wn0 Rd0 1wn0 R9u 1wqu U10 1tz0 TVu 1tz0 17gu 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cJu 1cL0 1cN0 1fz0 1cN0 1cOu 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cJu 1cL0 1cN0 1fz0 1cN0 1cL0 KEp0|57e4","Asia/Magadan|LMT MAGT MAGT MAGST MAGST MAGT|-a3.c -a0 -b0 -c0 -b0 -c0|0123232323232323232323241232323232323232323232323232323232323232512|-1Pca3.c eUo3.c 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3Cq0|95e3","Asia/Makassar|LMT MMT WITA JST|-7V.A -7V.A -80 -90|01232|-21JjV.A vfc0 myLV.A 8ML0|15e5","Asia/Manila|PHT PHST JST|-80 -90 -90|010201010|-1kJI0 AL0 cK10 65X0 mXB0 vX0 VK10 1db0|24e6","Asia/Nicosia|LMT EET EEST|-2d.s -20 -30|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1Vc2d.s 2a3cd.s 1cL0 1qp0 Xz0 19B0 19X0 1fB0 1db0 1cp0 1cL0 1fB0 19X0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1o30 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|32e4","Asia/Novokuznetsk|LMT +06 +07 +08|-5M.M -60 -70 -80|012323232323232323232321232323232323232323232323232323232323212|-1PctM.M eULM.M 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 2sp0 WM0|55e4","Asia/Novosibirsk|LMT +06 +07 +08|-5v.E -60 -70 -80|0123232323232323232323212323212121212121212121212121212121212121212|-21Qnv.E pAFv.E 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 ml0 Os0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 4eN0|15e5","Asia/Omsk|LMT OMST OMST OMSST OMSST OMST|-4R.u -50 -60 -70 -60 -70|012323232323232323232324123232323232323232323232323232323232323252|-224sR.u pMLR.u 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|12e5","Asia/Oral|LMT +04 +05 +06|-3p.o -40 -50 -60|01232323232323232121212121212121212121212121212|-1Pc3p.o eUnp.o 23CL0 3Db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 2pB0 1cM0 1fA0 1cM0 1cM0 IM0 1EM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0|27e4","Asia/Pontianak|LMT PMT WIB JST WIB WITA WIB|-7h.k -7h.k -7u -90 -80 -80 -70|012324256|-2ua7h.k XE00 munL.k 8Rau 6kpu 4PXu xhcu Wqnu|23e4","Asia/Pyongyang|LMT KST JCST JST KST|-8n -8u -90 -90 -90|012341|-2um8n 97XR 12FXu jdA0 2Onc0|29e5","Asia/Qyzylorda|LMT +04 +05 +06|-4l.Q -40 -50 -60|0123232323232323232323232323232323232323232323|-1Pc4l.Q eUol.Q 23CL0 3Db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 3ao0 1EM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0|73e4","Asia/Rangoon|RMT BURT JST MMT|-6o.E -6u -90 -6u|0123|-21Jio.E SmnS.E 7j9u|48e5","Asia/Sakhalin|LMT JCST JST SAKT SAKST SAKST SAKT|-9u.M -90 -90 -b0 -c0 -b0 -a0|01234343434343434343434356343434343435656565656565656565656565656363|-2AGVu.M 1iaMu.M je00 1qFa0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o10 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3rd0|58e4","Asia/Samarkand|LMT SAMT SAMT SAMST TAST UZST UZT|-4r.R -40 -50 -60 -60 -60 -50|01234323232323232323232356|-1Pc4r.R eUor.R 23CL0 1db0 1cM0 1dc0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11x0 bf0|36e4","Asia/Seoul|LMT KST JCST JST KST KDT KDT|-8r.Q -8u -90 -90 -90 -9u -a0|01234151515151515146464|-2um8r.Q 97XV.Q 12FXu jjA0 kKo0 2I0u OL0 1FB0 Rb0 1qN0 TX0 1tB0 TX0 1tB0 TX0 1tB0 TX0 2ap0 12FBu 11A0 1o00 11A0|23e6","Asia/Singapore|SMT MALT MALST MALT MALT JST SGT SGT|-6T.p -70 -7k -7k -7u -90 -7u -80|012345467|-2Bg6T.p 17anT.p 7hXE dM00 17bO 8Fyu Mspu DTA0|56e5","Asia/Srednekolymsk|LMT MAGT MAGT MAGST MAGST MAGT SRET|-ae.Q -a0 -b0 -c0 -b0 -c0 -b0|012323232323232323232324123232323232323232323232323232323232323256|-1Pcae.Q eUoe.Q 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|35e2","Asia/Taipei|JWST JST CST CDT|-80 -90 -80 -90|01232323232323232323232323232323232323232|-1iw80 joM0 1yo0 Tz0 1ip0 1jX0 1cN0 11b0 1oN0 11b0 1oN0 11b0 1oN0 11b0 10N0 1BX0 10p0 1pz0 10p0 1pz0 10p0 1db0 1dd0 1db0 1cN0 1db0 1cN0 1db0 1cN0 1db0 1BB0 ML0 1Bd0 ML0 uq10 1db0 1cN0 1db0 97B0 AL0|74e5","Asia/Tashkent|LMT TAST TAST TASST TASST UZST UZT|-4B.b -50 -60 -70 -60 -60 -50|01232323232323232323232456|-1Pc4B.b eUnB.b 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 11y0 bf0|23e5","Asia/Tbilisi|TBMT TBIT TBIT TBIST TBIST GEST GET GET GEST|-2X.b -30 -40 -50 -40 -40 -30 -40 -50|0123232323232323232323245656565787878787878787878567|-1Pc2X.b 1jUnX.b WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 3y0 19f0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cM0 1cL0 1fB0 3Nz0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 An0 Os0 WM0|11e5","Asia/Tehran|LMT TMT IRST IRST IRDT IRDT|-3p.I -3p.I -3u -40 -50 -4u|01234325252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252|-2btDp.I 1d3c0 1huLT.I TXu 1pz0 sN0 vAu 1cL0 1dB0 1en0 pNB0 UL0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 64p0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0|14e6","Asia/Thimphu|LMT IST BTT|-5W.A -5u -60|012|-Su5W.A 1BGMs.A|79e3","Asia/Tokyo|JCST JST JDT|-90 -90 -a0|0121212121|-1iw90 pKq0 QL0 1lB0 13X0 1zB0 NX0 1zB0 NX0|38e6","Asia/Tomsk|LMT +06 +07 +08|-5D.P -60 -70 -80|0123232323232323232323212323232323232323232323212121212121212121212|-21NhD.P pxzD.P 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 co0 1bB0 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3Qp0|10e5","Asia/Ulaanbaatar|LMT ULAT ULAT ULAST|-77.w -70 -80 -90|012323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-2APH7.w 2Uko7.w cKn0 1db0 1dd0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 6hD0 11z0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 kEp0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|12e5","Asia/Ust-Nera|LMT YAKT YAKT MAGST MAGT MAGST MAGT MAGT VLAT VLAT|-9w.S -80 -90 -c0 -b0 -b0 -a0 -c0 -b0 -a0|0123434343434343434343456434343434343434343434343434343434343434789|-21Q9w.S pApw.S 23CL0 1d90 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 17V0 7zD0|65e2","Asia/Vladivostok|LMT VLAT VLAT VLAST VLAST VLAT|-8L.v -90 -a0 -b0 -a0 -b0|012323232323232323232324123232323232323232323232323232323232323252|-1SJIL.v itXL.v 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|60e4","Asia/Yakutsk|LMT YAKT YAKT YAKST YAKST YAKT|-8C.W -80 -90 -a0 -90 -a0|012323232323232323232324123232323232323232323232323232323232323252|-21Q8C.W pAoC.W 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|28e4","Asia/Yekaterinburg|LMT PMT SVET SVET SVEST SVEST YEKT YEKST YEKT|-42.x -3J.5 -40 -50 -60 -50 -50 -60 -60|0123434343434343434343435267676767676767676767676767676767676767686|-2ag42.x 7mQh.s qBvJ.5 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|14e5","Asia/Yerevan|LMT YERT YERT YERST YERST AMST AMT AMT AMST|-2W -30 -40 -50 -40 -40 -30 -40 -50|0123232323232323232323245656565657878787878787878787878787878787|-1Pc2W 1jUnW WCL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1am0 2r0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 3Fb0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|13e5","Atlantic/Azores|HMT AZOT AZOST AZOMT AZOT AZOST WET|1S.w 20 10 0 10 0 0|01212121212121212121212121212121212121212121232123212321232121212121212121212121212121212121212121454545454545454545454545454545456545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2ldW5.s aPX5.s Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 qIl0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cL0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|25e4","Atlantic/Bermuda|LMT AST ADT|4j.i 40 30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1BnRE.G 1LTbE.G 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|65e3","Atlantic/Canary|LMT CANT WET WEST|11.A 10 0 -10|01232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-1UtaW.o XPAW.o 1lAK0 1a10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|54e4","Atlantic/Cape_Verde|LMT CVT CVST CVT|1y.4 20 10 10|01213|-2xomp.U 1qOMp.U 7zX0 1djf0|50e4","Atlantic/Faroe|LMT WET WEST|r.4 0 -10|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2uSnw.U 2Wgow.U 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|49e3","Atlantic/Madeira|FMT MADT MADST MADMT WET WEST|17.A 10 0 -10 0 -10|01212121212121212121212121212121212121212121232123212321232121212121212121212121212121212121212121454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2ldWQ.o aPWQ.o Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 qIl0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|27e4","Atlantic/Reykjavik|LMT IST ISST GMT|1s 10 0 0|012121212121212121212121212121212121212121212121212121212121212121213|-2uWmw mfaw 1Bd0 ML0 1LB0 Cn0 1LB0 3fX0 C10 HrX0 1cO0 LB0 1EL0 LA0 1C00 Oo0 1wo0 Rc0 1wo0 Rc0 1wo0 Rc0 1zc0 Oo0 1zc0 14o0 1lc0 14o0 1lc0 14o0 1o00 11A0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1lc0 14o0 1o00 14o0|12e4","Atlantic/South_Georgia|GST|20|0||30","Atlantic/Stanley|SMT FKT FKST FKT FKST|3P.o 40 30 30 20|0121212121212134343212121212121212121212121212121212121212121212121212|-2kJw8.A 12bA8.A 19X0 1fB0 19X0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 Cn0 1Cc10 WL0 1qL0 U10 1tz0 U10 1qM0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1tz0 U10 1tz0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1tz0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qL0 WN0 1qN0 U10 1wn0 Rd0 1wn0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1tz0 U10 1wn0 U10 1tz0 U10 1tz0 U10|21e2","Australia/Sydney|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 14o0 1o00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 11A0 1o00 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|40e5","Australia/Adelaide|ACST ACDT|-9u -au|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 Oo0 1zc0 WM0 1qM0 Rc0 1zc0 U00 1tA0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|11e5","Australia/Brisbane|AEST AEDT|-a0 -b0|01010101010101010|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 H1A0 Oo0 1zc0 Oo0 1zc0 Oo0|20e5","Australia/Broken_Hill|ACST ACDT|-9u -au|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 14o0 1o00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1tA0 WM0 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|18e3","Australia/Currie|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-29E80 19X0 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|746","Australia/Darwin|ACST ACDT|-9u -au|010101010|-293lt xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0|12e4","Australia/Eucla|ACWST ACWDT|-8J -9J|0101010101010101010|-293kI xcX 10jd0 yL0 1cN0 1cL0 1gSp0 Oo0 l5A0 Oo0 iJA0 G00 zU00 IM0 1qM0 11A0 1o00 11A0|368","Australia/Hobart|AEST AEDT|-a0 -b0|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-29E80 19X0 10jd0 yL0 1cN0 1cL0 1fB0 19X0 VfB0 1cM0 1o00 Rc0 1wo0 Rc0 1wo0 U00 1wo0 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 11A0 1qM0 WM0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1wo0 WM0 1tA0 WM0 1tA0 U00 1tA0 U00 1tA0 11A0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 11A0 1o00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|21e4","Australia/Lord_Howe|AEST LHST LHDT LHDT|-a0 -au -bu -b0|0121212121313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313|raC0 1zdu Rb0 1zd0 On0 1zd0 On0 1zd0 On0 1zd0 TXu 1qMu WLu 1tAu WLu 1tAu TXu 1tAu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu 11zu 1o0u 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 11Au 1nXu 1qMu 11zu 1o0u 11zu 1o0u 11zu 1qMu WLu 1qMu 11zu 1o0u WLu 1qMu 14nu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1fzu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu|347","Australia/Lindeman|AEST AEDT|-a0 -b0|010101010101010101010|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 H1A0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0|10","Australia/Melbourne|AEST AEDT|-a0 -b0|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101|-293lX xcX 10jd0 yL0 1cN0 1cL0 1fB0 19X0 17c10 LA0 1C00 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 U00 1qM0 WM0 1qM0 11A0 1tA0 U00 1tA0 U00 1tA0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 11A0 1o00 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 14o0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|39e5","Australia/Perth|AWST AWDT|-80 -90|0101010101010101010|-293jX xcX 10jd0 yL0 1cN0 1cL0 1gSp0 Oo0 l5A0 Oo0 iJA0 G00 zU00 IM0 1qM0 11A0 1o00 11A0|18e5","CET|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","CST6CDT|CST CDT CWT CPT|60 50 50 50|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261s0 1nX0 11B0 1nX0 SgN0 8x30 iw0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Pacific/Easter|EMT EAST EASST EAST EASST|7h.s 70 60 60 50|0121212121212121212121212121234343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-1uSgG.w 1s4IG.w WL0 1zd0 On0 1ip0 11z0 1o10 11z0 1qN0 WL0 1ld0 14n0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 WL0 1qN0 1cL0 1cN0 11z0 1o10 11z0 1qN0 WL0 1fB0 19X0 1qN0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1ip0 1fz0 1fB0 11z0 1qN0 WL0 1qN0 WL0 1qN0 WL0 1qN0 11z0 1o10 11z0 1o10 11z0 1qN0 WL0 1qN0 17b0 1ip0 11z0 1o10 19X0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Dd0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0|30e2","EET|EET EEST|-20 -30|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|hDB0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","EST|EST|50|0|","EST5EDT|EST EDT EWT EPT|50 40 40 40|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261t0 1nX0 11B0 1nX0 SgN0 8x40 iv0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Europe/Dublin|DMT IST GMT BST IST|p.l -y.D 0 -10 -10|01232323232324242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242|-2ax9y.D Rc0 1fzy.D 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 g5X0 14p0 1wn0 17d0 1io0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1a00 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1tA0 IM0 90o0 U00 1tA0 U00 1tA0 U00 1tA0 U00 1tA0 WM0 1qM0 WM0 1qM0 WM0 1tA0 U00 1tA0 U00 1tA0 11z0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 14o0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5","Etc/GMT+0|GMT|0|0|","Etc/GMT+1|GMT+1|10|0|","Etc/GMT+10|GMT+10|a0|0|","Etc/GMT+11|GMT+11|b0|0|","Etc/GMT+12|GMT+12|c0|0|","Etc/GMT+2|GMT+2|20|0|","Etc/GMT+3|GMT+3|30|0|","Etc/GMT+4|GMT+4|40|0|","Etc/GMT+5|GMT+5|50|0|","Etc/GMT+6|GMT+6|60|0|","Etc/GMT+7|GMT+7|70|0|","Etc/GMT+8|GMT+8|80|0|","Etc/GMT+9|GMT+9|90|0|","Etc/GMT-1|GMT-1|-10|0|","Etc/GMT-10|GMT-10|-a0|0|","Etc/GMT-11|GMT-11|-b0|0|","Etc/GMT-12|GMT-12|-c0|0|","Etc/GMT-13|GMT-13|-d0|0|","Etc/GMT-14|GMT-14|-e0|0|","Etc/GMT-2|GMT-2|-20|0|","Etc/GMT-3|GMT-3|-30|0|","Etc/GMT-4|GMT-4|-40|0|","Etc/GMT-5|GMT-5|-50|0|","Etc/GMT-6|GMT-6|-60|0|","Etc/GMT-7|GMT-7|-70|0|","Etc/GMT-8|GMT-8|-80|0|","Etc/GMT-9|GMT-9|-90|0|","Etc/UCT|UCT|0|0|","Etc/UTC|UTC|0|0|","Europe/Amsterdam|AMT NST NEST NET CEST CET|-j.w -1j.w -1k -k -20 -10|010101010101010101010101010101010101010101012323234545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545|-2aFcj.w 11b0 1iP0 11A0 1io0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1co0 1io0 1yo0 Pc0 1a00 1fA0 1Bc0 Mo0 1tc0 Uo0 1tA0 U00 1uo0 W00 1s00 VA0 1so0 Vc0 1sM0 UM0 1wo0 Rc0 1u00 Wo0 1rA0 W00 1s00 VA0 1sM0 UM0 1w00 fV0 BCX.w 1tA0 U00 1u00 Wo0 1sm0 601k WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|16e5","Europe/Andorra|WET CET CEST|0 -10 -20|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-UBA0 1xIN0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|79e3","Europe/Astrakhan|LMT +03 +04 +05|-3c.c -30 -40 -50|012323232323232323212121212121212121212121212121212121212121212|-1Pcrc.c eUMc.c 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 2pB0 1cM0 1fA0 1cM0 3Co0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3rd0","Europe/Athens|AMT EET EEST CEST CET|-1y.Q -20 -30 -20 -10|012123434121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2a61x.Q CNbx.Q mn0 kU10 9b0 3Es0 Xa0 1fb0 1dd0 k3X0 Nz0 SCp0 1vc0 SO0 1cM0 1a00 1ao0 1fc0 1a10 1fG0 1cg0 1dX0 1bX0 1cQ0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|35e5","Europe/London|GMT BST BDST|0 -10 -20|0101010101010101010101010101010101010101010101010121212121210101210101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2axa0 Rc0 1fA0 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 2Rz0 Dc0 1zc0 Oo0 1zc0 Rc0 1wo0 17c0 1iM0 FA0 xB0 1fA0 1a00 14o0 bb0 LA0 xB0 Rc0 1wo0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1a00 1qM0 WM0 1qM0 11A0 1o00 WM0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1tA0 IM0 90o0 U00 1tA0 U00 1tA0 U00 1tA0 U00 1tA0 WM0 1qM0 WM0 1qM0 WM0 1tA0 U00 1tA0 U00 1tA0 11z0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 14o0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|10e6","Europe/Belgrade|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-19RC0 3IP0 WM0 1fA0 1cM0 1cM0 1rc0 Qo0 1vmo0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5","Europe/Berlin|CET CEST CEMT|-10 -20 -30|01010101010101210101210101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 kL0 Nc0 m10 WM0 1ao0 1cp0 dX0 jz0 Dd0 1io0 17c0 1fA0 1a00 1ehA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|41e5","Europe/Prague|CET CEST|-10 -20|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 16M0 1lc0 1tA0 17A0 11c0 1io0 17c0 1io0 17c0 1fc0 1ao0 1bNc0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|13e5","Europe/Brussels|WET CET CEST WEST|0 -10 -20 -10|0121212103030303030303030303030303030303030303030303212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ehc0 3zX0 11c0 1iO0 11A0 1o00 11A0 my0 Ic0 1qM0 Rc0 1EM0 UM0 1u00 10o0 1io0 1io0 17c0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a30 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 y00 5Wn0 WM0 1fA0 1cM0 16M0 1iM0 16M0 1C00 Uo0 1eeo0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|21e5","Europe/Bucharest|BMT EET EEST|-1I.o -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1xApI.o 20LI.o RA0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Axc0 On0 1fA0 1a10 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cK0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cL0 1cN0 1cL0 1fB0 1nX0 11E0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|19e5","Europe/Budapest|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1ip0 17b0 1op0 1tb0 Q2m0 3Ne0 WM0 1fA0 1cM0 1cM0 1oJ0 1dc0 1030 1fA0 1cM0 1cM0 1cM0 1cM0 1fA0 1a00 1iM0 1fA0 8Ha0 Rb0 1wN0 Rb0 1BB0 Lz0 1C20 LB0 SNX0 1a10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|17e5","Europe/Zurich|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-19Lc0 11A0 1o00 11A0 1xG10 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|38e4","Europe/Chisinau|CMT BMT EET EEST CEST CET MSK MSD|-1T -1I.o -20 -30 -20 -10 -30 -40|012323232323232323234545467676767676767676767323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232|-26jdT wGMa.A 20LI.o RA0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 27A0 2en0 39g0 WM0 1fA0 1cM0 V90 1t7z0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 gL0 WO0 1cM0 1cM0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1nX0 11D0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|67e4","Europe/Copenhagen|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2azC0 Tz0 VuO0 60q0 WM0 1fA0 1cM0 1cM0 1cM0 S00 1HA0 Nc0 1C00 Dc0 1Nc0 Ao0 1h5A0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5","Europe/Gibraltar|GMT BST BDST CET CEST|0 -10 -20 -10 -20|010101010101010101010101010101010101010101010101012121212121010121010101010101010101034343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-2axa0 Rc0 1fA0 14M0 1fc0 1g00 1co0 1dc0 1co0 1oo0 1400 1dc0 19A0 1io0 1io0 WM0 1o00 14o0 1o00 17c0 1io0 17c0 1fA0 1a00 1lc0 17c0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1cM0 1io0 17c0 1fA0 1a00 1io0 17c0 1io0 17c0 1fA0 1a00 1io0 1qM0 Dc0 2Rz0 Dc0 1zc0 Oo0 1zc0 Rc0 1wo0 17c0 1iM0 FA0 xB0 1fA0 1a00 14o0 bb0 LA0 xB0 Rc0 1wo0 11A0 1o00 17c0 1fA0 1a00 1fA0 1cM0 1fA0 1a00 17c0 1fA0 1a00 1io0 17c0 1lc0 17c0 1fA0 10Jz0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|30e3","Europe/Helsinki|HMT EET EEST|-1D.N -20 -30|0121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-1WuND.N OULD.N 1dA0 1xGq0 1cM0 1cM0 1cM0 1cN0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5","Europe/Kaliningrad|CET CEST CET CEST MSK MSD EEST EET FET|-10 -20 -20 -30 -30 -40 -30 -20 -30|0101010101010232454545454545454546767676767676767676767676767676767676767676787|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 Am0 Lb0 1en0 op0 1pNz0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|44e4","Europe/Kiev|KMT EET MSK CEST CET MSD EEST|-22.4 -20 -30 -20 -10 -40 -30|0123434252525252525252525256161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161|-1Pc22.4 eUo2.4 rnz0 2Hg0 WM0 1fA0 da0 1v4m0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 Db0 3220 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|34e5","Europe/Kirov|LMT +03 +04 +05|-3i.M -30 -40 -50|01232323232323232321212121212121212121212121212121212121212121|-22WNi.M qHai.M 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 2pB0 1cM0 1fA0 1cM0 3Co0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|48e4","Europe/Lisbon|LMT WET WEST WEMT CET CEST|A.J 0 -10 -20 -10 -20|012121212121212121212121212121212121212121212321232123212321212121212121212121212121212121212121214121212121212121212121212121212124545454212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ldXn.f aPWn.f Sp0 LX0 1vc0 Tc0 1uM0 SM0 1vc0 Tc0 1vc0 SM0 1vc0 6600 1co0 3E00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 3I00 17c0 1cM0 1cM0 3Fc0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 1tA0 1cM0 1dc0 1400 gL0 IM0 s10 U00 dX0 Rc0 pd0 Rc0 gL0 Oo0 pd0 Rc0 gL0 Oo0 pd0 14o0 1cM0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 3Co0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 pvy0 1cM0 1cM0 1fA0 1cM0 1cM0 1cN0 1cL0 1cN0 1cM0 1cM0 1cM0 1cM0 1cN0 1cL0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|27e5","Europe/Luxembourg|LMT CET CEST WET WEST WEST WET|-o.A -10 -20 0 -10 -20 -10|0121212134343434343434343434343434343434343434343434565651212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2DG0o.A t6mo.A TB0 1nX0 Up0 1o20 11A0 rW0 CM0 1qP0 R90 1EO0 UK0 1u20 10m0 1ip0 1in0 17e0 19W0 1fB0 1db0 1cp0 1in0 17d0 1fz0 1a10 1in0 1a10 1in0 17f0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Dc0 vA0 60L0 WM0 1fA0 1cM0 17c0 1io0 16M0 1C00 Uo0 1eeo0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|54e4","Europe/Madrid|WET WEST WEMT CET CEST|0 -10 -20 -10 -20|01010101010101010101010121212121234343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343|-28dd0 11A0 1go0 19A0 1co0 1dA0 b1A0 18o0 3I00 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 iyo0 Rc0 18o0 1hc0 1io0 1a00 14o0 5aL0 MM0 1vc0 17A0 1i00 1bc0 1eo0 17d0 1in0 17A0 6hA0 10N0 XIL0 1a10 1in0 17d0 19X0 1cN0 1fz0 1a10 1fX0 1cp0 1cO0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|62e5","Europe/Malta|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2as10 M00 1cM0 1cM0 14o0 1o00 WM0 1qM0 17c0 1cM0 M3A0 5M20 WM0 1fA0 1cM0 1cM0 1cM0 16m0 1de0 1lc0 14m0 1lc0 WO0 1qM0 GTW0 On0 1C10 Lz0 1C10 Lz0 1EN0 Lz0 1C10 Lz0 1zd0 Oo0 1C00 On0 1cp0 1cM0 1lA0 Xc0 1qq0 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1o10 11z0 1iN0 19z0 1fB0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|42e4","Europe/Minsk|MMT EET MSK CEST CET MSD EEST FET|-1O -20 -30 -20 -10 -40 -30 -30|012343432525252525252525252616161616161616161616161616161616161616172|-1Pc1O eUnO qNX0 3gQ0 WM0 1fA0 1cM0 Al0 1tsn0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 3Fc0 1cN0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hy0|19e5","Europe/Monaco|PMT WET WEST WEMT CET CEST|-9.l 0 -10 -20 -10 -20|01212121212121212121212121212121212121212121212121232323232345454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-2nco9.l cNb9.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 1u00 10o0 1io0 1wo0 Rc0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Df0 2RV0 11z0 11B0 1ze0 WM0 1fA0 1cM0 1fa0 1aq0 16M0 1ekn0 1cL0 1fC0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|38e3","Europe/Moscow|MMT MMT MST MDST MSD MSK MSM EET EEST MSK|-2u.h -2v.j -3v.j -4v.j -40 -30 -50 -20 -30 -40|012132345464575454545454545454545458754545454545454545454545454545454545454595|-2ag2u.h 2pyW.W 1bA0 11X0 GN0 1Hb0 c20 imv.j 3DA0 dz0 15A0 c10 2q10 iM10 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|16e6","Europe/Paris|PMT WET WEST CEST CET WEMT|-9.l 0 -10 -20 -10 -20|0121212121212121212121212121212121212121212121212123434352543434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434|-2nco8.l cNb8.l HA0 19A0 1iM0 11c0 1oo0 Wo0 1rc0 QM0 1EM0 UM0 1u00 10o0 1io0 1wo0 Rc0 1a00 1fA0 1cM0 1cM0 1io0 17c0 1fA0 1a00 1io0 1a00 1io0 17c0 1fA0 1a00 1io0 17c0 1cM0 1cM0 1a00 1io0 1cM0 1cM0 1a00 1fA0 1io0 17c0 1cM0 1cM0 1a00 1fA0 1io0 1qM0 Df0 Ik0 5M30 WM0 1fA0 1cM0 Vx0 hB0 1aq0 16M0 1ekn0 1cL0 1fC0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|11e6","Europe/Riga|RMT LST EET MSK CEST CET MSD EEST|-1A.y -2A.y -20 -30 -20 -10 -40 -30|010102345454536363636363636363727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272|-25TzA.y 11A0 1iM0 ko0 gWm0 yDXA.y 2bX0 3fE0 WM0 1fA0 1cM0 1cM0 4m0 1sLy0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cN0 1o00 11A0 1o00 11A0 1qM0 3oo0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|64e4","Europe/Rome|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2as10 M00 1cM0 1cM0 14o0 1o00 WM0 1qM0 17c0 1cM0 M3A0 5M20 WM0 1fA0 1cM0 16K0 1iO0 16m0 1de0 1lc0 14m0 1lc0 WO0 1qM0 GTW0 On0 1C10 Lz0 1C10 Lz0 1EN0 Lz0 1C10 Lz0 1zd0 Oo0 1C00 On0 1C10 Lz0 1zd0 On0 1C10 LA0 1C00 LA0 1zc0 Oo0 1C00 Oo0 1zc0 Oo0 1fC0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|39e5","Europe/Samara|LMT SAMT SAMT KUYT KUYST MSD MSK EEST SAMST SAMST|-3k.k -30 -40 -40 -50 -40 -30 -30 -50 -40|012343434343434343435656712828282828282828282828282828282828282912|-22WNk.k qHak.k bcn0 1Qqo0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cN0 8o0 14m0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qN0 WM0|12e5","Europe/Simferopol|SMT EET MSK CEST CET MSD EEST MSK|-2g -20 -30 -20 -10 -40 -30 -40|012343432525252525252525252161616525252616161616161616161616161616161616172|-1Pc2g eUog rEn0 2qs0 WM0 1fA0 1cM0 3V0 1u0L0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Q00 4eL0 1cL0 1cN0 1cL0 1cN0 dX0 WL0 1cN0 1cL0 1fB0 1o30 11B0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11z0 1nW0|33e4","Europe/Sofia|EET CET CEST EEST|-20 -10 -20 -30|01212103030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030|-168L0 WM0 1fA0 1cM0 1cM0 1cN0 1mKH0 1dd0 1fb0 1ap0 1fb0 1a20 1fy0 1a30 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cK0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 1nX0 11E0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5","Europe/Stockholm|CET CEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2azC0 TB0 2yDe0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|15e5","Europe/Tallinn|TMT CET CEST EET MSK MSD EEST|-1D -10 -20 -20 -30 -40 -30|012103421212454545454545454546363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363|-26oND teD 11A0 1Ta0 4rXl KSLD 2FX0 2Jg0 WM0 1fA0 1cM0 18J0 1sTX0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o10 11A0 1qM0 5QM0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|41e4","Europe/Tirane|LMT CET CEST|-1j.k -10 -20|01212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2glBj.k 14pcj.k 5LC0 WM0 4M0 1fCK0 10n0 1op0 11z0 1pd0 11z0 1qN0 WL0 1qp0 Xb0 1qp0 Xb0 1qp0 11z0 1lB0 11z0 1qN0 11z0 1iN0 16n0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|42e4","Europe/Ulyanovsk|LMT +03 +04 +05 +02|-3d.A -30 -40 -50 -20|01232323232323232321214121212121212121212121212121212121212121212|-22WNd.A qHad.A 23CL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 2pB0 1cM0 1fA0 2pB0 IM0 rX0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0 3rd0","Europe/Uzhgorod|CET CEST MSK MSD EET EEST|-10 -20 -30 -40 -20 -30|010101023232323232323232320454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454|-1cqL0 6i00 WM0 1fA0 1cM0 1ml0 1Cp0 1r3W0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1Q00 1Nf0 2pw0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|11e4","Europe/Vienna|CET CEST|-10 -20|0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 3KM0 14o0 LA00 6i00 WM0 1fA0 1cM0 1cM0 1cM0 400 2qM0 1a00 1cM0 1cM0 1io0 17c0 1gHa0 19X0 1cP0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|18e5","Europe/Vilnius|WMT KMT CET EET MSK CEST MSD EEST|-1o -1z.A -10 -20 -30 -20 -40 -30|012324525254646464646464646473737373737373737352537373737373737373737373737373737373737373737373737373737373737373737373|-293do 6ILM.o 1Ooz.A zz0 Mfd0 29W0 3is0 WM0 1fA0 1cM0 LV0 1tgL0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11B0 1o00 11A0 1qM0 8io0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|54e4","Europe/Volgograd|LMT TSAT STAT STAT VOLT VOLST VOLST VOLT MSD MSK MSK|-2V.E -30 -30 -40 -40 -50 -40 -30 -40 -30 -40|0123454545454545454676767489898989898989898989898989898989898989a9|-21IqV.E cLXV.E cEM0 1gqn0 Lco0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1cM0 1cM0 1fA0 1cM0 2pz0 1cN0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 8Hz0|10e5","Europe/Warsaw|WMT CET CEST EET EEST|-1o -10 -20 -20 -30|012121234312121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121|-2ctdo 1LXo 11d0 1iO0 11A0 1o00 11A0 1on0 11A0 6zy0 HWP0 5IM0 WM0 1fA0 1cM0 1dz0 1mL0 1en0 15B0 1aq0 1nA0 11A0 1io0 17c0 1fA0 1a00 iDX0 LA0 1cM0 1cM0 1C00 Oo0 1cM0 1cM0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1C00 LA0 uso0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cN0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|17e5","Europe/Zaporozhye|CUT EET MSK CEST CET MSD EEST|-2k -20 -30 -20 -10 -40 -30|01234342525252525252525252526161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161|-1Pc2k eUok rdb0 2RE0 WM0 1fA0 8m0 1v9a0 1db0 1cN0 1db0 1cN0 1db0 1dd0 1cO0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cK0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cQ0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|77e4","HST|HST|a0|0|","Indian/Chagos|LMT IOT IOT|-4N.E -50 -60|012|-2xosN.E 3AGLN.E|30e2","Indian/Christmas|CXT|-70|0||21e2","Indian/Cocos|CCT|-6u|0||596","Indian/Kerguelen|-00 TFT|0 -50|01|-MG00|130","Indian/Mahe|LMT SCT|-3F.M -40|01|-2yO3F.M|79e3","Indian/Maldives|MMT MVT|-4S -50|01|-olgS|35e4","Indian/Mauritius|LMT MUT MUST|-3O -40 -50|012121|-2xorO 34unO 14L0 12kr0 11z0|15e4","Indian/Reunion|LMT RET|-3F.Q -40|01|-2mDDF.Q|84e4","Pacific/Kwajalein|MHT KWAT MHT|-b0 c0 -c0|012|-AX0 W9X0|14e3","MET|MET MEST|-10 -20|01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-2aFe0 11d0 1iO0 11A0 1o00 11A0 Qrc0 6i00 WM0 1fA0 1cM0 1cM0 1cM0 16M0 1gMM0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00","MST|MST|70|0|","MST7MDT|MST MDT MWT MPT|70 60 60 60|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261r0 1nX0 11B0 1nX0 SgN0 8x20 ix0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Pacific/Chatham|CHAST CHAST CHADT|-cf -cJ -dJ|012121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212|-WqAf 1adef IM0 1C00 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1qM0 14o0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1lc0 14o0 1lc0 14o0 1lc0 17c0 1io0 17c0 1io0 17c0 1io0 17c0 1io0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|600","PST8PDT|PST PDT PWT PPT|80 70 70 70|010102301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|-261q0 1nX0 11B0 1nX0 SgN0 8x10 iy0 QwN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1cN0 1cL0 1cN0 1cL0 s10 1Vz0 LB0 1BX0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0","Pacific/Apia|LMT WSST SST SDT WSDT WSST|bq.U bu b0 a0 -e0 -d0|01232345454545454545454545454545454545454545454545454545454|-2nDMx.4 1yW03.4 2rRbu 1ff0 1a00 CI0 AQ0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|37e3","Pacific/Bougainville|PGT JST BST|-a0 -90 -b0|0102|-16Wy0 7CN0 2MQp0|18e4","Pacific/Chuuk|CHUT|-a0|0||49e3","Pacific/Efate|LMT VUT VUST|-bd.g -b0 -c0|0121212121212121212121|-2l9nd.g 2Szcd.g 1cL0 1oN0 10L0 1fB0 19X0 1fB0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1cN0 1cL0 1fB0 Lz0 1Nd0 An0|66e3","Pacific/Enderbury|PHOT PHOT PHOT|c0 b0 -d0|012|nIc0 B8n0|1","Pacific/Fakaofo|TKT TKT|b0 -d0|01|1Gfn0|483","Pacific/Fiji|LMT FJT FJST|-bT.I -c0 -d0|0121212121212121212121212121212121212121212121212121212121212121|-2bUzT.I 3m8NT.I LA0 1EM0 IM0 nJc0 LA0 1o00 Rc0 1wo0 Ao0 1Nc0 Ao0 1Q00 xz0 1SN0 uM0 1SM0 uM0 1VA0 s00 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1VA0 s00 1VA0 s00 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1VA0 s00 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0 1VA0 s00 1VA0 s00 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0 uM0|88e4","Pacific/Funafuti|TVT|-c0|0||45e2","Pacific/Galapagos|LMT ECT GALT|5W.o 50 60|012|-1yVS1.A 2dTz1.A|25e3","Pacific/Gambier|LMT GAMT|8X.M 90|01|-2jof0.c|125","Pacific/Guadalcanal|LMT SBT|-aD.M -b0|01|-2joyD.M|11e4","Pacific/Guam|GST ChST|-a0 -a0|01|1fpq0|17e4","Pacific/Honolulu|HST HDT HST|au 9u a0|010102|-1thLu 8x0 lef0 8Pz0 46p0|37e4","Pacific/Kiritimati|LINT LINT LINT|aE a0 -e0|012|nIaE B8nk|51e2","Pacific/Kosrae|KOST KOST|-b0 -c0|010|-AX0 1bdz0|66e2","Pacific/Majuro|MHT MHT|-b0 -c0|01|-AX0|28e3","Pacific/Marquesas|LMT MART|9i 9u|01|-2joeG|86e2","Pacific/Pago_Pago|LMT NST BST SST|bm.M b0 b0 b0|0123|-2nDMB.c 2gVzB.c EyM0|37e2","Pacific/Nauru|LMT NRT JST NRT|-b7.E -bu -90 -c0|01213|-1Xdn7.E PvzB.E 5RCu 1ouJu|10e3","Pacific/Niue|NUT NUT NUT|bk bu b0|012|-KfME 17y0a|12e2","Pacific/Norfolk|NMT NFT NFST NFT|-bc -bu -cu -b0|01213|-Kgbc W01G On0 1COp0|25e4","Pacific/Noumea|LMT NCT NCST|-b5.M -b0 -c0|01212121|-2l9n5.M 2EqM5.M xX0 1PB0 yn0 HeP0 Ao0|98e3","Pacific/Palau|PWT|-90|0||21e3","Pacific/Pitcairn|PNT PST|8u 80|01|18Vku|56","Pacific/Pohnpei|PONT|-b0|0||34e3","Pacific/Port_Moresby|PGT|-a0|0||25e4","Pacific/Rarotonga|CKT CKHST CKT|au 9u a0|012121212121212121212121212|lyWu IL0 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Onu 1zcu Rbu 1zcu Onu 1zcu Onu 1zcu Onu|13e3","Pacific/Tahiti|LMT TAHT|9W.g a0|01|-2joe1.I|18e4","Pacific/Tarawa|GILT|-c0|0||29e3","Pacific/Tongatapu|TOT TOT TOST|-ck -d0 -e0|01212121|-1aB0k 2n5dk 15A0 1wo0 xz0 1Q10 xz0|75e3","Pacific/Wake|WAKT|-c0|0||16e3","Pacific/Wallis|WFT|-c0|0||94","WET|WET WEST|0 -10|010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010|hDB0 1a00 1fA0 1cM0 1cM0 1cM0 1fA0 1a00 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00"], + links:["Africa/Abidjan|Africa/Bamako","Africa/Abidjan|Africa/Banjul","Africa/Abidjan|Africa/Conakry","Africa/Abidjan|Africa/Dakar","Africa/Abidjan|Africa/Freetown","Africa/Abidjan|Africa/Lome","Africa/Abidjan|Africa/Nouakchott","Africa/Abidjan|Africa/Ouagadougou","Africa/Abidjan|Africa/Sao_Tome","Africa/Abidjan|Africa/Timbuktu","Africa/Abidjan|Atlantic/St_Helena","Africa/Cairo|Egypt","Africa/Johannesburg|Africa/Maseru","Africa/Johannesburg|Africa/Mbabane","Africa/Khartoum|Africa/Juba","Africa/Lagos|Africa/Bangui","Africa/Lagos|Africa/Brazzaville","Africa/Lagos|Africa/Douala","Africa/Lagos|Africa/Kinshasa","Africa/Lagos|Africa/Libreville","Africa/Lagos|Africa/Luanda","Africa/Lagos|Africa/Malabo","Africa/Lagos|Africa/Niamey","Africa/Lagos|Africa/Porto-Novo","Africa/Maputo|Africa/Blantyre","Africa/Maputo|Africa/Bujumbura","Africa/Maputo|Africa/Gaborone","Africa/Maputo|Africa/Harare","Africa/Maputo|Africa/Kigali","Africa/Maputo|Africa/Lubumbashi","Africa/Maputo|Africa/Lusaka","Africa/Nairobi|Africa/Addis_Ababa","Africa/Nairobi|Africa/Asmara","Africa/Nairobi|Africa/Asmera","Africa/Nairobi|Africa/Dar_es_Salaam","Africa/Nairobi|Africa/Djibouti","Africa/Nairobi|Africa/Kampala","Africa/Nairobi|Africa/Mogadishu","Africa/Nairobi|Indian/Antananarivo","Africa/Nairobi|Indian/Comoro","Africa/Nairobi|Indian/Mayotte","Africa/Tripoli|Libya","America/Adak|America/Atka","America/Adak|US/Aleutian","America/Anchorage|US/Alaska","America/Argentina/Buenos_Aires|America/Buenos_Aires","America/Argentina/Catamarca|America/Argentina/ComodRivadavia","America/Argentina/Catamarca|America/Catamarca","America/Argentina/Cordoba|America/Cordoba","America/Argentina/Cordoba|America/Rosario","America/Argentina/Jujuy|America/Jujuy","America/Argentina/Mendoza|America/Mendoza","America/Atikokan|America/Coral_Harbour","America/Chicago|US/Central","America/Curacao|America/Aruba","America/Curacao|America/Kralendijk","America/Curacao|America/Lower_Princes","America/Denver|America/Shiprock","America/Denver|Navajo","America/Denver|US/Mountain","America/Detroit|US/Michigan","America/Edmonton|Canada/Mountain","America/Fort_Wayne|America/Indiana/Indianapolis","America/Fort_Wayne|America/Indianapolis","America/Fort_Wayne|US/East-Indiana","America/Halifax|Canada/Atlantic","America/Havana|Cuba","America/Indiana/Knox|America/Knox_IN","America/Indiana/Knox|US/Indiana-Starke","America/Jamaica|Jamaica","America/Kentucky/Louisville|America/Louisville","America/Los_Angeles|US/Pacific","America/Los_Angeles|US/Pacific-New","America/Manaus|Brazil/West","America/Mazatlan|Mexico/BajaSur","America/Mexico_City|Mexico/General","America/New_York|US/Eastern","America/Noronha|Brazil/DeNoronha","America/Panama|America/Cayman","America/Phoenix|US/Arizona","America/Port_of_Spain|America/Anguilla","America/Port_of_Spain|America/Antigua","America/Port_of_Spain|America/Dominica","America/Port_of_Spain|America/Grenada","America/Port_of_Spain|America/Guadeloupe","America/Port_of_Spain|America/Marigot","America/Port_of_Spain|America/Montserrat","America/Port_of_Spain|America/St_Barthelemy","America/Port_of_Spain|America/St_Kitts","America/Port_of_Spain|America/St_Lucia","America/Port_of_Spain|America/St_Thomas","America/Port_of_Spain|America/St_Vincent","America/Port_of_Spain|America/Tortola","America/Port_of_Spain|America/Virgin","America/Regina|Canada/East-Saskatchewan","America/Regina|Canada/Saskatchewan","America/Rio_Branco|America/Porto_Acre","America/Rio_Branco|Brazil/Acre","America/Santiago|Chile/Continental","America/Sao_Paulo|Brazil/East","America/St_Johns|Canada/Newfoundland","America/Tijuana|America/Ensenada","America/Tijuana|America/Santa_Isabel","America/Tijuana|Mexico/BajaNorte","America/Toronto|America/Montreal","America/Toronto|Canada/Eastern","America/Vancouver|Canada/Pacific","America/Whitehorse|Canada/Yukon","America/Winnipeg|Canada/Central","Asia/Ashgabat|Asia/Ashkhabad","Asia/Bangkok|Asia/Phnom_Penh","Asia/Bangkok|Asia/Vientiane","Asia/Dhaka|Asia/Dacca","Asia/Dubai|Asia/Muscat","Asia/Ho_Chi_Minh|Asia/Saigon","Asia/Hong_Kong|Hongkong","Asia/Jerusalem|Asia/Tel_Aviv","Asia/Jerusalem|Israel","Asia/Kathmandu|Asia/Katmandu","Asia/Kolkata|Asia/Calcutta","Asia/Macau|Asia/Macao","Asia/Makassar|Asia/Ujung_Pandang","Asia/Nicosia|Europe/Nicosia","Asia/Qatar|Asia/Bahrain","Asia/Riyadh|Asia/Aden","Asia/Riyadh|Asia/Kuwait","Asia/Seoul|ROK","Asia/Shanghai|Asia/Chongqing","Asia/Shanghai|Asia/Chungking","Asia/Shanghai|Asia/Harbin","Asia/Shanghai|PRC","Asia/Singapore|Singapore","Asia/Taipei|ROC","Asia/Tehran|Iran","Asia/Thimphu|Asia/Thimbu","Asia/Tokyo|Japan","Asia/Ulaanbaatar|Asia/Ulan_Bator","Asia/Urumqi|Asia/Kashgar","Atlantic/Faroe|Atlantic/Faeroe","Atlantic/Reykjavik|Iceland","Australia/Adelaide|Australia/South","Australia/Brisbane|Australia/Queensland","Australia/Broken_Hill|Australia/Yancowinna","Australia/Darwin|Australia/North","Australia/Hobart|Australia/Tasmania","Australia/Lord_Howe|Australia/LHI","Australia/Melbourne|Australia/Victoria","Australia/Perth|Australia/West","Australia/Sydney|Australia/ACT","Australia/Sydney|Australia/Canberra","Australia/Sydney|Australia/NSW","Etc/GMT+0|Etc/GMT","Etc/GMT+0|Etc/GMT-0","Etc/GMT+0|Etc/GMT0","Etc/GMT+0|Etc/Greenwich","Etc/GMT+0|GMT","Etc/GMT+0|GMT+0","Etc/GMT+0|GMT-0","Etc/GMT+0|GMT0","Etc/GMT+0|Greenwich","Etc/UCT|UCT","Etc/UTC|Etc/Universal","Etc/UTC|Etc/Zulu","Etc/UTC|UTC","Etc/UTC|Universal","Etc/UTC|Zulu","Europe/Belgrade|Europe/Ljubljana","Europe/Belgrade|Europe/Podgorica","Europe/Belgrade|Europe/Sarajevo","Europe/Belgrade|Europe/Skopje","Europe/Belgrade|Europe/Zagreb","Europe/Chisinau|Europe/Tiraspol","Europe/Dublin|Eire","Europe/Helsinki|Europe/Mariehamn","Europe/Istanbul|Asia/Istanbul","Europe/Istanbul|Turkey","Europe/Lisbon|Portugal","Europe/London|Europe/Belfast","Europe/London|Europe/Guernsey","Europe/London|Europe/Isle_of_Man","Europe/London|Europe/Jersey","Europe/London|GB","Europe/London|GB-Eire","Europe/Moscow|W-SU","Europe/Oslo|Arctic/Longyearbyen","Europe/Oslo|Atlantic/Jan_Mayen","Europe/Prague|Europe/Bratislava","Europe/Rome|Europe/San_Marino","Europe/Rome|Europe/Vatican","Europe/Warsaw|Poland","Europe/Zurich|Europe/Busingen","Europe/Zurich|Europe/Vaduz","Pacific/Auckland|Antarctica/McMurdo","Pacific/Auckland|Antarctica/South_Pole","Pacific/Auckland|NZ","Pacific/Chatham|NZ-CHAT","Pacific/Chuuk|Pacific/Truk","Pacific/Chuuk|Pacific/Yap","Pacific/Easter|Chile/EasterIsland","Pacific/Guam|Pacific/Saipan","Pacific/Honolulu|Pacific/Johnston","Pacific/Honolulu|US/Hawaii","Pacific/Kwajalein|Kwajalein","Pacific/Pago_Pago|Pacific/Midway","Pacific/Pago_Pago|Pacific/Samoa","Pacific/Pago_Pago|US/Samoa","Pacific/Pohnpei|Pacific/Ponape"]}),a}); From 125c2180312cb2a6bf79dee3bad706bd22b3764a Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 26 May 2017 16:05:30 +0300 Subject: [PATCH 123/363] MAGETWO-63157: [Backport] - Product image gallery look-ups use incorrect ID - for 2.1 --- .../Model/Product/Type/Configurable.php | 21 ------------------- .../Model/Product/Type/ConfigurableTest.php | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 0eb81d8d826d4..2ea6574b9cf17 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -1359,25 +1359,4 @@ public function getSalableUsedProducts(Product $product, $requiredAttributeIds = return $usedSalableProducts; } - - /** - * @inheritdoc - */ - public function isPossibleBuyFromList($product) - { - /** @var bool $isAllCustomOptionsDisplayed */ - $isAllCustomOptionsDisplayed = true; - - foreach ($this->getConfigurableAttributes($product) as $attribute) { - /** @var Attribute $eavAttribute */ - $eavAttribute = $attribute->getProductAttribute(); - - $isAllCustomOptionsDisplayed = ( - $isAllCustomOptionsDisplayed - && $eavAttribute->getData('used_in_product_listing') - ); - } - - return $isAllCustomOptionsDisplayed; - } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 75d4e049551f9..0fadbc66db7da 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -1,6 +1,6 @@ Date: Fri, 26 May 2017 16:13:18 +0300 Subject: [PATCH 124/363] MAGETWO-63589: [Backport] - [Github] Imported configurable products with multiple super attributes do not retain super attribute ordering #6079 - for 2.1 --- .../Model/Export/RowCustomizer.php | 47 ++- .../Import/Product/Type/Configurable.php | 5 +- .../Unit/Model/Export/RowCustomizerTest.php | 318 +++++++++--------- 3 files changed, 182 insertions(+), 188 deletions(-) diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php index 09c2400f437f4..ec5a3f71716b2 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php @@ -6,7 +6,9 @@ namespace Magento\ConfigurableImportExport\Model\Export; use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; -use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\ImportExport\Model\Import; class RowCustomizer implements RowCustomizerInterface @@ -19,29 +21,23 @@ class RowCustomizer implements RowCustomizerInterface /** * Prepare configurable data for export * - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param ProductCollection $collection * @param int[] $productIds * @return void */ public function prepareData($collection, $productIds) { $productCollection = clone $collection; - $productCollection->addAttributeToFilter( - 'entity_id', - ['in' => $productIds] - )->addAttributeToFilter( - 'type_id', - ['eq' => \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE] - ); + $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) + ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); while ($product = $productCollection->fetchItem()) { $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); + $this->configurableData[$product->getId()] = []; + $variations = []; + $variationsLabels = []; foreach ($productAttributesOptions as $productAttributeOption) { - $this->configurableData[$product->getId()] = []; - $variations = []; - $variationsLabels = []; - foreach ($productAttributeOption as $optValues) { $variations[$optValues['sku']][] = $optValues['attribute_code'] . '=' . $optValues['option_title']; @@ -50,20 +46,21 @@ public function prepareData($collection, $productIds) $optValues['attribute_code'] . '=' . $optValues['super_attribute_label']; } } + } - foreach ($variations as $sku => $values) { - $variations[$sku] = - 'sku=' . $sku . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $values); - } - $variations = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $variations); - $variationsLabels = implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $variationsLabels); - - $this->configurableData[$product->getId()] = [ - 'configurable_variations' => $variations, - 'configurable_variation_labels' => $variationsLabels, - ]; + foreach ($variations as $sku => $values) { + $variations[$sku] = + 'sku=' . $sku . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $values); } + + $this->configurableData[$product->getId()] = [ + 'configurable_variations' => implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $variations), + 'configurable_variation_labels' => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + $variationsLabels + ) + ]; } } 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 4da2e903e7c1f..f5ef67ff72d7f 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -495,6 +495,7 @@ protected function _parseVariations($rowData) } if (!empty($fieldAndValuePairs['sku'])) { + $position = 0; $additionalRow['_super_products_sku'] = $fieldAndValuePairs['sku']; unset($fieldAndValuePairs['sku']); $additionalRow['display'] = isset($fieldAndValuePairs['display']) ? $fieldAndValuePairs['display'] : 1; @@ -502,8 +503,10 @@ protected function _parseVariations($rowData) foreach ($fieldAndValuePairs as $attrCode => $attrValue) { $additionalRow['_super_attribute_code'] = $attrCode; $additionalRow['_super_attribute_option'] = $attrValue; + $additionalRow['_super_attribute_position'] = $position; $additionalRows[] = $additionalRow; $additionalRow = []; + $position += 1; } } } @@ -709,7 +712,7 @@ protected function _collectSuperDataLabels($data, $productSuperAttrId, $productI $attrParams = $this->_superAttributes[$data['_super_attribute_code']]; $this->_superAttributesData['attributes'][$productId][$attrParams['id']] = [ 'product_super_attribute_id' => $productSuperAttrId, - 'position' => 0, + 'position' => $data['_super_attribute_position'], ]; $label = isset($variationLabels[$data['_super_attribute_code']]) ? $variationLabels[$data['_super_attribute_code']] diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php index ab4d1fd3b5d8b..73a659784fc59 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -3,51 +3,60 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\ConfigurableImportExport\Test\Unit\Model\Export; -use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ConfigurableImportExport\Model\Export\RowCustomizer as ExportRowCustomizer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\Catalog\Model\Product; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\ImportExport\Model\Import; class RowCustomizerTest extends \PHPUnit_Framework_TestCase { /** - * Test existing product id. - * - * @var int + * @var ExportRowCustomizer */ - protected $initiatedProductId = 11; + private $exportRowCustomizer; /** - * @var \Magento\ConfigurableImportExport\Model\Export\RowCustomizer + * @var ObjectManagerHelper */ - protected $_model; + private $objectManagerHelper; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var ProductCollection|\PHPUnit_Framework_MockObject_MockObject */ - protected $_collectionMock; + private $productCollectionMock; + + /** + * @var ConfigurableProductType|\PHPUnit_Framework_MockObject_MockObject + */ + private $configurableProductTypeMock; + + /** + * @var int + */ + private $productId = 11; protected function setUp() { - $this->_collectionMock = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\Collection', - ['addAttributeToFilter', 'fetchItem', '__wakeup'], - [], - '', - false - ); - $this->_model = new \Magento\ConfigurableImportExport\Model\Export\RowCustomizer(); - } + $this->productCollectionMock = $this->getMockBuilder(ProductCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configurableProductTypeMock = $this->getMockBuilder(ConfigurableProductType::class) + ->disableOriginalConstructor() + ->getMock(); - public function testPrepareData() - { - $this->_initConfigurableData(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->exportRowCustomizer = $this->objectManagerHelper->getObject(ExportRowCustomizer::class); } public function testAddHeaderColumns() { - $this->_initConfigurableData(); + $this->initConfigurableData(); + $this->assertEquals( [ 'column_1', @@ -56,37 +65,80 @@ public function testAddHeaderColumns() 'configurable_variations', 'configurable_variation_labels', ], - $this->_model->addHeaderColumns( - ['column_1', 'column_2', 'column_3'] - ) + $this->exportRowCustomizer->addHeaderColumns(['column_1', 'column_2', 'column_3']) ); } /** * @param array $expected * @param array $data + * * @dataProvider addDataDataProvider */ public function testAddData(array $expected, array $data) { - $this->_initConfigurableData(); - $this->assertEquals( - $expected, - $this->_model->addData($data['data_row'], $data['product_id']) - ); + $this->initConfigurableData(); + + $this->assertEquals($expected, $this->exportRowCustomizer->addData($data['data_row'], $data['product_id'])); + } + + /** + * @return array + */ + public function addDataDataProvider() + { + $expectedConfigurableData = $this->getExpectedConfigurableData(); + $data = $expectedConfigurableData[$this->productId]; + + return [ + [ + '$expected' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + '$data' => [ + 'data_row' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + 'product_id' => 1 + ] + ], + [ + '$expected' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3', + 'configurable_variations' => $data['configurable_variations'], + 'configurable_variation_labels' => $data['configurable_variation_labels'] + ], + '$data' => [ + 'data_row' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + 'product_id' => $this->productId + ] + ] + ]; } /** * @param array $expected * @param array $data + * * @dataProvider getAdditionalRowsCountDataProvider */ public function testGetAdditionalRowsCount(array $expected, array $data) { - $this->_initConfigurableData(); + $this->initConfigurableData(); + $this->assertEquals( $expected, - $this->_model->getAdditionalRowsCount($data['row_count'], $data['product_id']) + $this->exportRowCustomizer->getAdditionalRowsCount($data['row_count'], $data['product_id']) ); } @@ -101,7 +153,7 @@ public function getAdditionalRowsCountDataProvider() [ 'row_count' => [1, 2, 3], 'product_id' => 1 - ], + ] ], [ [1, 2, 3], @@ -120,131 +172,66 @@ public function getAdditionalRowsCountDataProvider() ]; } - /** - * @return array - */ - public function addDataDataProvider() - { - $expectedConfigurableData = $this->getExpectedConfigurableData(); - $data = $expectedConfigurableData[$this->initiatedProductId]; - - return [ - [ - '$expected' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - '$data' => [ - 'data_row' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - 'product_id' => 1 - ], - ], - [ - '$expected' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - 'configurable_variations' => $data['configurable_variations'], - 'configurable_variation_labels' => $data['configurable_variation_labels'], - ], - '$data' => [ - 'data_row' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - 'product_id' => $this->initiatedProductId - ] - ] - ]; - } - - protected function _initConfigurableData() + private function initConfigurableData() { $productIds = [1, 2, 3]; + $expectedConfigurableData = $this->getExpectedConfigurableData(); + $productMock = $this->createProductMock(); $productAttributesOptions = [ - [//1 $productAttributeOption - [//1opt $optValue + [ + [ 'pricing_is_percent' => true, 'sku' => '_sku_', 'attribute_code' => 'code_of_attribute', 'option_title' => 'Option Title', 'pricing_value' => 112345, - 'super_attribute_label' => 'Super attribute label', + 'super_attribute_label' => 'Super attribute label' ], - [//2opt $optValue + [ 'pricing_is_percent' => false, 'sku' => '_sku_', 'attribute_code' => 'code_of_attribute', 'option_title' => 'Option Title', 'pricing_value' => 212345, - 'super_attribute_label' => '', + 'super_attribute_label' => '' ], - [//3opt $optValue + [ 'pricing_is_percent' => false, 'sku' => '_sku_2', 'attribute_code' => 'code_of_attribute_2', 'option_title' => 'Option Title 2', 'pricing_value' => 312345, - 'super_attribute_label' => 'Super attribute label 2', - ], - ], + 'super_attribute_label' => 'Super attribute label 2' + ] + ] ]; - $expectedConfigurableData = $this->getExpectedConfigurableData(); - - $productMock = $this->getMock( - 'Magento\Catalog\Model\Product', - ['getId', 'getTypeInstance', '__wakeup'], - [], - '', - false - ); - $productMock->expects($this->any()) + $productMock->expects(static::any()) ->method('getId') - ->will($this->returnValue($this->initiatedProductId)); - - $typeInstanceMock = $this->getMock( - 'Magento\ConfigurableProduct\Model\Product\Type\Configurable', - [], - [], - '', - false - ); - $typeInstanceMock->expects($this->any()) - ->method('getConfigurableOptions') - ->will($this->returnValue($productAttributesOptions)); - - $productMock->expects($this->any()) + ->willReturn($this->productId); + $productMock->expects(static::any()) ->method('getTypeInstance') - ->will($this->returnValue($typeInstanceMock)); - - $this->_collectionMock->expects($this->at(0)) - ->method('addAttributeToFilter') - ->with('entity_id', ['in' => $productIds]) - ->will($this->returnSelf()); - $this->_collectionMock->expects($this->at(1)) + ->willReturn($this->configurableProductTypeMock); + $this->configurableProductTypeMock->expects(static::any()) + ->method('getConfigurableOptions') + ->willReturn($productAttributesOptions); + $this->productCollectionMock->expects(static::atLeastOnce()) ->method('addAttributeToFilter') - ->with('type_id', ['eq' => \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE]) - ->will($this->returnSelf()); - $this->_collectionMock->expects($this->at(2)) - ->method('fetchItem') - ->will($this->returnValue($productMock)); - $this->_collectionMock->expects($this->at(3)) + ->willReturnMap( + [ + ['entity_id', ['in' => $productIds], 'inner', $this->productCollectionMock], + ['type_id', ['eq' => ConfigurableProductType::TYPE_CODE], 'inner', $this->productCollectionMock] + ] + ); + $this->productCollectionMock->expects(static::atLeastOnce()) ->method('fetchItem') - ->will($this->returnValue(false)); + ->willReturnOnConsecutiveCalls($productMock, false); - - $this->_model->prepareData($this->_collectionMock, $productIds); - - $configurableData = $this->getPropertyValue($this->_model, 'configurableData'); - - $this->assertEquals($expectedConfigurableData, $configurableData); + $this->exportRowCustomizer->prepareData($this->productCollectionMock, $productIds); + $this->assertEquals( + $expectedConfigurableData, + $this->getPropertyValue($this->exportRowCustomizer, 'configurableData') + ); } /** @@ -252,49 +239,56 @@ protected function _initConfigurableData() * * @return array */ - protected function getExpectedConfigurableData() + private function getExpectedConfigurableData() { return [ - $this->initiatedProductId => [ - 'configurable_variations' => implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, [ - '_sku_' => 'sku=_sku_' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute=Option Title', - 'code_of_attribute=Option Title', - ]), - '_sku_2' => 'sku=_sku_2' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute_2=Option Title 2', - ]) - ]), - 'configurable_variation_labels' => implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute' => 'code_of_attribute=Super attribute label', - 'code_of_attribute_2' => 'code_of_attribute_2=Super attribute label 2', - ]), - ], + $this->productId => [ + 'configurable_variations' => implode( + ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, + [ + '_sku_' => 'sku=_sku_' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + ['code_of_attribute=Option Title', 'code_of_attribute=Option Title'] + ), + '_sku_2' => 'sku=_sku_2' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + ['code_of_attribute_2=Option Title 2'] + ) + ] + ), + 'configurable_variation_labels' => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + [ + 'code_of_attribute' => 'code_of_attribute=Super attribute label', + 'code_of_attribute_2' => 'code_of_attribute_2=Super attribute label 2' + ] + ) + ] ]; } /** - * @param $object - * @param $property - * @param $value + * Create product mock object + * + * @return Product|\PHPUnit_Framework_MockObject_MockObject */ - protected function setPropertyValue(&$object, $property, $value) + private function createProductMock() { - $reflection = new \ReflectionClass(get_class($object)); - $reflectionProperty = $reflection->getProperty($property); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); - return $object; + return $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); } /** - * @param $object - * @param $property + * Get value of protected property + * + * @param object $object + * @param string $property * @return mixed */ - protected function getPropertyValue(&$object, $property) + private function getPropertyValue($object, $property) { $reflection = new \ReflectionClass(get_class($object)); $reflectionProperty = $reflection->getProperty($property); From 37294182fac62fda6e9bff040425eef985620639 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 26 May 2017 16:28:05 +0300 Subject: [PATCH 125/363] MAGETWO-63157: [Backport] - Product image gallery look-ups use incorrect ID - for 2.1 --- .../Model/Product/Type/ConfigurableTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 0c42f0885ef0e..e6cf95a05575b 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -256,7 +256,8 @@ public function testGetSelectedAttributesInfo() $product->addCustomOption('attributes', serialize([$attribute['attribute_id'] => $optionValueId])); $info = $this->model->getSelectedAttributesInfo($product); - $this->assertEquals([['label' => 'Test Configurable', 'value' => 'Option 1']], $info); + $this->assertEquals([['label' => 'Test Configurable', 'value' => 'Option 1', + 'option_id' => 193, 'option_value' => '45']], $info); } public function testGetSelectedAttributesInfoForStore() @@ -273,7 +274,8 @@ public function testGetSelectedAttributesInfoForStore() $attribute->getProductAttribute()->setStoreLabel('store label'); $info = $this->model->getSelectedAttributesInfo($this->product); - $this->assertEquals([['label' => 'store label', 'value' => 'Option 1']], $info); + $this->assertEquals([['label' => 'store label', 'value' => 'Option 1', + 'option_id' => 194, 'option_value' => '47']], $info); } /** @@ -317,7 +319,7 @@ public function testGetOrderOptions() $this->assertArrayHasKey('info_buyRequest', $result); $this->assertArrayHasKey('attributes_info', $result); $this->assertEquals( - [['label' => 'Test Configurable', 'value' => 'Option 1']], + [['label' => 'Test Configurable', 'value' => 'Option 1', 'option_id' => 197, 'option_value' => '53']], $result['attributes_info'] ); $this->assertArrayHasKey('product_calculations', $result); From 137e78c34db41898d114e3d2f8594587415dbb8e Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Fri, 26 May 2017 16:53:19 +0300 Subject: [PATCH 126/363] MAGETWO-61267: Change format date for update and add ability to run cron from functional test --- dev/tests/functional/.htaccess | 6 ------ .../TestSuite/InjectableTests/MAGETWO-61267.xml | 15 --------------- 2 files changed, 21 deletions(-) delete mode 100644 dev/tests/functional/.htaccess delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml diff --git a/dev/tests/functional/.htaccess b/dev/tests/functional/.htaccess deleted file mode 100644 index ce860a0805f4d..0000000000000 --- a/dev/tests/functional/.htaccess +++ /dev/null @@ -1,6 +0,0 @@ -########################################### -## Allow access to command.php - - order allow,deny - allow from all - diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml deleted file mode 100644 index ed87b9a3205e2..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61267.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 53b259063aa627c79e22f3465f43ed8d49c8c734 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 26 May 2017 17:25:02 +0300 Subject: [PATCH 127/363] MAGETWO-63589: [Backport] - [Github] Imported configurable products with multiple super attributes do not retain super attribute ordering #6079 - for 2.1 --- .../Test/Unit/Model/Export/RowCustomizerTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php index 73a659784fc59..df61bb3e02a1b 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -13,6 +13,11 @@ use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\ImportExport\Model\Import; +/** + * Tests \Magento\ConfigurableImportExport\Model\Export\RowCustomizer + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RowCustomizerTest extends \PHPUnit_Framework_TestCase { /** From a4da7705220410f6b23de4701a721953673a2869 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 26 May 2017 17:55:30 +0300 Subject: [PATCH 128/363] MAGETWO-63157: [Backport] - Product image gallery look-ups use incorrect ID - for 2.1 --- .../Model/Product/Type/ConfigurableTest.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index e6cf95a05575b..8c872d263e551 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -257,7 +257,8 @@ public function testGetSelectedAttributesInfo() $product->addCustomOption('attributes', serialize([$attribute['attribute_id'] => $optionValueId])); $info = $this->model->getSelectedAttributesInfo($product); $this->assertEquals([['label' => 'Test Configurable', 'value' => 'Option 1', - 'option_id' => 193, 'option_value' => '45']], $info); + 'option_id' => $attribute['attribute_id'], 'option_value' => $attribute['values'][0]['value_index']]], + $info); } public function testGetSelectedAttributesInfoForStore() @@ -275,7 +276,8 @@ public function testGetSelectedAttributesInfoForStore() $attribute->getProductAttribute()->setStoreLabel('store label'); $info = $this->model->getSelectedAttributesInfo($this->product); $this->assertEquals([['label' => 'store label', 'value' => 'Option 1', - 'option_id' => 194, 'option_value' => '47']], $info); + 'option_id' => $attribute['attribute_id'], 'option_value' => $optionValueId]], + $info); } /** @@ -316,10 +318,15 @@ public function testGetOrderOptions() $product = $this->_prepareForCart(); $result = $this->model->getOrderOptions($product); + + $attributes = $this->model->getConfigurableAttributesAsArray($this->product); + $attribute = reset($attributes); + $this->assertArrayHasKey('info_buyRequest', $result); $this->assertArrayHasKey('attributes_info', $result); $this->assertEquals( - [['label' => 'Test Configurable', 'value' => 'Option 1', 'option_id' => 197, 'option_value' => '53']], + [['label' => 'Test Configurable', 'value' => 'Option 1', + 'option_id' => $attribute['attribute_id'], 'option_value' => $attribute['values'][0]['value_index']]], $result['attributes_info'] ); $this->assertArrayHasKey('product_calculations', $result); From 0c691f45e94fe95a296171b11a3e8daa592bf7a0 Mon Sep 17 00:00:00 2001 From: Abu Date: Sun, 28 May 2017 03:14:38 +0530 Subject: [PATCH 129/363] #9139 Unable to set negative product's quantity fixes commit. --- .../Product/Form/Modifier/AdvancedInventory.php | 2 +- .../view/adminhtml/ui_component/product_form.xml | 2 +- .../adminhtml/web/js/components/qty-validator-changer.js | 3 +-- .../Magento/Ui/view/base/web/js/lib/validation/rules.js | 9 +++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) 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 94ded128ec06f..6fd92299ac75b 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 @@ -213,7 +213,7 @@ private function prepareMeta() 'dataScope' => 'qty', 'validation' => [ 'validate-number' => true, - 'validate-digits' => true, + 'validate-integer' => true, 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, ], 'imports' => [ 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 af7ad35e0df54..b72b88d2d7269 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 @@ -95,7 +95,7 @@ quantity_and_stock_status.qty true - true + true 99999999 200 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 f11e020e72337..3123784e64e71 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 @@ -19,8 +19,7 @@ define([ handleChanges: function (value) { var isDigits = value !== 1; - this.validation['validate-number'] = !isDigits; - this.validation['validate-digits'] = isDigits; + this.validation['validate-integer'] = isDigits; this.validation['less-than-equals-to'] = isDigits ? 99999999 : 99999999.9999; this.validate(); } 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 4702120860285..d256357bfad2c 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 @@ -572,6 +572,15 @@ define([ }, $.mage.__('Please enter a valid number in this field.') ], + "validate-integer": [ + function(value) { + return ( + utils.isEmptyNoTrim(value) + || (!isNaN(utils.parseNumber(value)) && /^\s*-?\d*\s*$/.test(value)) + ); + }, + $.mage.__('Please enter a valid integer in this field.') + ], "validate-number-range": [ function(value, param) { if (utils.isEmptyNoTrim(value)) { From b30f00226808e351d682a74c6f39c647ef381254 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Mon, 29 May 2017 10:51:02 +0300 Subject: [PATCH 130/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../Catalog/Test/Unit/Block/Product/View/AttributesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php index 7e93222722034..091711d5cb064 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php @@ -18,7 +18,7 @@ class AttributesTest extends \PHPUnit_Framework_TestCase * * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $objectManager; + private $objectManager; /** * Attributes block. From 4b5c93299b371c5bdedc5cfd63f8972116e66d1b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 29 May 2017 11:24:08 +0300 Subject: [PATCH 131/363] MAGETWO-62091: [Backport] - Configurable products simple products get removed on product save - for 2.1 --- app/code/Magento/Ui/view/base/web/js/form/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Ui/view/base/web/js/form/client.js b/app/code/Magento/Ui/view/base/web/js/form/client.js index 6b4c1a3262273..7aff72e719e72 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/client.js +++ b/app/code/Magento/Ui/view/base/web/js/form/client.js @@ -104,6 +104,7 @@ define([ _save: function (data, options) { var url = this.urls.save; + $('body').trigger('processStart'); options = options || {}; if (!options.redirect) { @@ -116,6 +117,8 @@ define([ data: data }, options); + $('body').trigger('processStop'); + return this; } From 188a3ba760ceb73725bf74544559fd2a61803d93 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 29 May 2017 14:49:16 +0300 Subject: [PATCH 132/363] MAGETWO-63589: [Backport] - [Github] Imported configurable products with multiple super attributes do not retain super attribute ordering #6079 - for 2.1 --- .../Import/Product/Type/ConfigurableTest.php | 71 +++++++++++++++++ .../Import/_files/configurable_attributes.php | 78 +++++++++++++++++++ .../configurable_attributes_rollback.php | 37 +++++++++ ...t_configurable_with_attributes_sorting.csv | 3 + 4 files changed, 189 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_with_attributes_sorting.csv diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php index 210369c8ac6af..7ef88b489249f 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php @@ -37,6 +37,18 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase */ protected $productMetadata; + /** + * Configurable product test Name. + */ + const TEST_PRODUCT_NAME = 'Configurable 1'; + + /** + * Configurable product options SKU list + * + * @var array + */ + protected $optionSkuList = ['Configurable 1-Option 2-Option 1']; + protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -157,4 +169,63 @@ public function testConfigurableImport($pathToFile, $productName, $optionSkuList $this->assertEquals(2, count($valuesData)); } } + + /** + * Tests that after import configurable products super attributes retain ordering. + * + * @magentoDataFixture Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php + * @magentoAppArea adminhtml + */ + public function testConfigurableWithAttributesSortingImport() + { + // import data from CSV file + $pathToFile = __DIR__ . '/../../_files/import_configurable_with_attributes_sorting.csv'; + $filesystem = $this->objectManager->create( + \Magento\Framework\Filesystem::class + ); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->model->setSource($source) + ->setParameters( + [ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + ) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->model->importData(); + + /** @var \Magento\Catalog\Model\ResourceModel\Product $resource */ + $resource = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); + $productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME); + $this->assertTrue(is_numeric($productId)); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->objectManager->get(ProductRepositoryInterface::class)->getById($productId); + $configurableProductOptions = $product->getExtensionAttributes()->getConfigurableProductOptions(); + + $attributesPositionExpectation = [ + 'test_attribute_2' => 0, + 'test_attribute_1' => 1, + ]; + + /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $configurableProductOption */ + foreach ($configurableProductOptions as $configurableProductOption) { + $productAttribute = $configurableProductOption->getProductAttribute(); + $productAttributeCode = $productAttribute->getAttributeCode(); + + if (isset($attributesPositionExpectation[$productAttributeCode])) { + $expectedPosition = $attributesPositionExpectation[$productAttributeCode]; + $this->assertEquals($expectedPosition, $configurableProductOption->getPosition()); + } + } + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php new file mode 100644 index 0000000000000..a55463a1205b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php @@ -0,0 +1,78 @@ +get(\Magento\Eav\Model\Config::class); +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +/** @var $attributes array */ +$attributes = [ + [ + 'code' => 'test_attribute_1', + 'label' => 'Test attribute 1' + ], + [ + 'code' => 'test_attribute_2', + 'label' => 'Test attribute 2' + ] +]; + +foreach ($attributes as $item) { + $code = $item['code']; + $label = $item['label']; + $attribute = $eavConfig->getAttribute('catalog_product', $code); + if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() + ) { + $attribute->delete(); + } + + $eavConfig->clear(); + + /* Create attribute */ + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + $attribute->setData( + [ + 'attribute_code' => $code, + 'entity_type_id' => 4, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => [$label], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attribute->save(); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} + +/** @var \Magento\Eav\Model\Config $eavConfig */ +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes_rollback.php new file mode 100644 index 0000000000000..b69899c567718 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes_rollback.php @@ -0,0 +1,37 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +/** @var $attributes array */ +$attributes = [ + [ + 'code' => 'test_attribute_1', + 'label' => 'Test attribute 1' + ], + [ + 'code' => 'test_attribute_2', + 'label' => 'Test attribute 2' + ] +]; + +foreach ($attributes as $attribute) { + $attribute = $eavConfig->getAttribute('catalog_product', $attribute['code']); + if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() + ) { + $attribute->delete(); + } +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_with_attributes_sorting.csv b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_with_attributes_sorting.csv new file mode 100644 index 0000000000000..e49dab899a6f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/import_configurable_with_attributes_sorting.csv @@ -0,0 +1,3 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,configurable_variations,configurable_variation_labels,associated_skus +Configurable 1-Option 2-Option 1,,Default,simple,Default Category/Test category,base,Configurable 1-Option 2-Option 1,,,1,1,Taxable Goods,Not Visible Individually,100,,,,configurable-1-option-2-option-1,Configurable 1,Configurable 1,Configurable 1 ,,,,,,,,,"5/3/17, 3:39 AM","5/3/17, 3:39 AM",,,Block after Info Column,,,,,,,,,,,,,"test_attribute_1=Option 1,test_attribute_2=Option 2",10,0,1,0,0,1,1,1,0,1,1,,1,1,1,1,0,1,0,0,1,0,1,,,,,,,,,,,,,, +Configurable 1,,Default,configurable,Default Category/Test category,base,Configurable 1,,,1,1,Taxable Goods,"Catalog, Search",,,,,configurable-1,Configurable 1,Configurable 1,Configurable 1 ,,,,,,,,,"5/3/17, 3:39 AM","5/3/17, 3:39 AM",,,Block after Info Column,,,,,,,,,,,,,,0,0,1,0,0,1,1,1,0,1,1,,1,1,1,1,0,1,0,0,1,0,1,,,,,,,,,,,,"sku=Configurable 1-Option 2-Option 1,test_attribute_2=Option 2,test_attribute_1=Option 1",, From 01f1cfc135c49c884ade64bbc91d9ac868a5d946 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 14:52:53 +0300 Subject: [PATCH 133/363] MAGETWO-61267: Change format date for update and add ability to run cron from functional test --- .../Unit/Component/Form/Element/DataType/DateTest.php | 11 ++++++++++- .../Catalog/Test/Repository/CatalogProductSimple.xml | 1 - lib/web/moment-timezone-with-data.js | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php index 7a9df1a7df0a4..19836acb41c4f 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php @@ -1,6 +1,6 @@ contextMock->expects($this->any())->method('getProcessor')->willReturn($this->processorMock); } + /** + * This tests ensures that outputDateFormat is properly saved in the configuration with timeOffset. + */ public function testPrepareWithTimeOffset() { $this->date = new Date( @@ -75,6 +81,9 @@ public function testPrepareWithTimeOffset() $this->assertEquals($localeDateFormat, $config['outputDateFormat']); } + /** + * This tests ensures that outputDateFormat is properly saved in the configuration without timeOffset. + */ public function testPrepareWithoutTimeOffset() { $defaultDateFormat = 'MM/dd/y'; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 05a0018f7f42a..151404d934407 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1233,6 +1233,5 @@ simple-product-%isolation% all - diff --git a/lib/web/moment-timezone-with-data.js b/lib/web/moment-timezone-with-data.js index 6dfd9559f93d4..59c9b7540ec49 100644 --- a/lib/web/moment-timezone-with-data.js +++ b/lib/web/moment-timezone-with-data.js @@ -1,3 +1,7 @@ +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ //! moment-timezone.js //! version : 0.5.5 //! author : Tim Wood From 87faab53761577acc0b9c5532ef48565a71110f8 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 14:53:46 +0300 Subject: [PATCH 134/363] MAGETWO-61267: Change format date for update and add ability to run cron from functional test --- .../Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php index 19836acb41c4f..9d37735bec196 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php @@ -126,7 +126,7 @@ public function testPrepareWithoutTimeOffset() } /** - * This tests ensures that userTimeZone is properly saved in the configuration + * This tests ensures that userTimeZone is properly saved in the configuration. */ public function testPrepare() { From 8ff7fd0e7a2a20f188086dbe165f7255905de82a Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 29 May 2017 15:24:24 +0300 Subject: [PATCH 135/363] MAGETWO-63589: [Backport] - [Github] Imported configurable products with multiple super attributes do not retain super attribute ordering #6079 - for 2.1 --- .../Model/Import/_files/configurable_attributes.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php index a55463a1205b2..5e0fbb8047a6f 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/_files/configurable_attributes.php @@ -7,7 +7,10 @@ /** @var \Magento\Eav\Model\Config $eavConfig */ $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); /** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create( + \Magento\Catalog\Setup\CategorySetup::class + ); /** @var $attributes array */ $attributes = [ From e32af8220551cf3d41932931051836ca6428c431 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 15:46:30 +0300 Subject: [PATCH 136/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Cms/Helper/Page.php | 2 +- .../Cms/Test/Unit/Controller/RouterTest.php | 166 ++++++++++++++ .../Magento/Cms/Test/Unit/Helper/PageTest.php | 55 +++-- app/code/Magento/Theme/Block/Html/Topmenu.php | 60 +++-- .../Test/Unit/Block/Html/TopmenuTest.php | 215 +++++++++++++++--- .../System/Design/Theme/IndexTest.php | 10 +- 6 files changed, 427 insertions(+), 81 deletions(-) create mode 100644 app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index cebaeb5c89559..2ff8a4317e19a 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -156,7 +156,7 @@ public function prepareResultPage(Action $action, $pageId = null) $this->_eventManager->dispatch( 'cms_page_render', - ['page' => $this->_page, 'controller_action' => $action] + ['page' => $this->_page, 'controller_action' => $action, 'request' => $this->_getRequest()] ); if ($this->_page->getCustomLayoutUpdateXml() && $inRange) { diff --git a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php new file mode 100644 index 0000000000000..8f68a7e4c2ecb --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php @@ -0,0 +1,166 @@ +eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->getMockForAbstractClass(); + + $this->pageFactoryMock = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->getMockForAbstractClass(); + + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->actionFactoryMock = $this->getMockBuilder(\Magento\Framework\App\ActionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->router = $objectManagerHelper->getObject( + \Magento\Cms\Controller\Router::class, + [ + 'eventManager' => $this->eventManagerMock, + 'pageFactory' => $this->pageFactoryMock, + 'storeManager' => $this->storeManagerMock, + 'actionFactory' => $this->actionFactoryMock, + ] + ); + } + + /** + * This tests ensures that cms controller router match cms page before event params. + */ + public function testMatchCmsControllerRouterMatchBeforeEventParams() + { + $identifier = '/test'; + $trimedIdentifier = 'test'; + $pageId = 1; + $storeId = 1; + + /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject $requestMock */ + $requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods([ + 'getPathInfo', + 'setModuleName', + 'setControllerName', + 'setActionName', + 'setParam', + 'setAlias', + ]) + ->getMockForAbstractClass(); + $requestMock->expects($this->once()) + ->method('getPathInfo') + ->willReturn($identifier); + $requestMock->expects($this->once()) + ->method('setModuleName') + ->with('cms') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setControllerName') + ->with('page') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setActionName') + ->with('view') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setParam') + ->with('page_id', $pageId) + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setAlias') + ->with(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, $trimedIdentifier) + ->willReturnSelf(); + + $condition = new \Magento\Framework\DataObject(['identifier' => $trimedIdentifier, 'continue' => true]); + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'cms_controller_router_match_before', + [ + 'router' => $this->router, + 'condition' => $condition, + ] + ) + ->willReturnSelf(); + + $pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) + ->disableOriginalConstructor() + ->getMock(); + $pageMock->expects($this->once()) + ->method('checkIdentifier') + ->with($trimedIdentifier, $storeId) + ->willReturn($pageId); + + $this->pageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($pageMock); + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $actionMock = $this->getMockBuilder(\Magento\Framework\App\ActionInterface::class) + ->getMockForAbstractClass(); + + $this->actionFactoryMock->expects($this->once()) + ->method('create') + ->with(\Magento\Framework\App\Action\Forward::class) + ->willReturn($actionMock); + + $this->assertEquals($actionMock, $this->router->match($requestMock)); + } +} \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php index ed379d9683086..77c80acacfad6 100644 --- a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php @@ -113,16 +113,21 @@ class PageTest extends \PHPUnit_Framework_TestCase */ protected $resultPageFactory; + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpRequestMock; + protected function setUp() { - $this->actionMock = $this->getMockBuilder('Magento\Framework\App\Action\Action') + $this->actionMock = $this->getMockBuilder(\Magento\Framework\App\Action\Action::class) ->disableOriginalConstructor() ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder('Magento\Cms\Model\PageFactory') + $this->pageFactoryMock = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->pageMock = $this->getMockBuilder('Magento\Cms\Model\Page') + $this->pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) ->disableOriginalConstructor() ->setMethods( [ @@ -141,57 +146,60 @@ protected function setUp() ] ) ->getMock(); - $this->storeManagerMock = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->localeDateMock = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime\TimezoneInterface') + $this->localeDateMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->getMockForAbstractClass(); - $this->designMock = $this->getMockBuilder('Magento\Framework\View\DesignInterface') + $this->designMock = $this->getMockBuilder(\Magento\Framework\View\DesignInterface::class) ->getMockForAbstractClass(); - $this->pageConfigMock = $this->getMockBuilder('Magento\Framework\View\Page\Config') + $this->pageConfigMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config::class) ->disableOriginalConstructor() ->getMock(); - $this->escaperMock = $this->getMockBuilder('Magento\Framework\Escaper') + $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->disableOriginalConstructor() ->getMock(); - $this->eventManagerMock = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->getMockForAbstractClass(); + $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) ->getMockForAbstractClass(); - $this->urlBuilderMock = $this->getMockBuilder('Magento\Framework\UrlInterface') + $this->httpRequestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->getMockForAbstractClass(); - $this->storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); - $this->resultPageMock = $this->getMockBuilder('Magento\Framework\View\Result\Page') + $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->disableOriginalConstructor() ->getMock(); - $this->layoutMock = $this->getMockBuilder('Magento\Framework\View\LayoutInterface') + $this->layoutMock = $this->getMockBuilder(\Magento\Framework\View\LayoutInterface::class) ->getMockForAbstractClass(); - $this->layoutProcessorMock = $this->getMockBuilder('Magento\Framework\View\Layout\ProcessorInterface') + $this->layoutProcessorMock = $this->getMockBuilder(\Magento\Framework\View\Layout\ProcessorInterface::class) ->getMockForAbstractClass(); - $this->blockMock = $this->getMockBuilder('Magento\Framework\View\Element\AbstractBlock') + $this->blockMock = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->messagesBlockMock = $this->getMockBuilder('Magento\Framework\View\Element\Messages') + $this->messagesBlockMock = $this->getMockBuilder(\Magento\Framework\View\Element\Messages::class) ->disableOriginalConstructor() ->getMock(); - $this->messageManagerMock = $this->getMockBuilder('Magento\Framework\Message\ManagerInterface') + $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->getMockForAbstractClass(); - $this->messageCollectionMock = $this->getMockBuilder('Magento\Framework\Message\Collection') + $this->messageCollectionMock = $this->getMockBuilder(\Magento\Framework\Message\Collection::class) ->disableOriginalConstructor() ->getMock(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( - 'Magento\Framework\App\Helper\Context', + \Magento\Framework\App\Helper\Context::class, [ 'eventManager' => $this->eventManagerMock, - 'urlBuilder' => $this->urlBuilderMock + 'urlBuilder' => $this->urlBuilderMock, + 'httpRequest' => $this->httpRequestMock, ] ); - $this->resultPageFactory = $this->getMock('Magento\Framework\View\Result\PageFactory', [], [], '', false); + $this->resultPageFactory = $this->getMock(\Magento\Framework\View\Result\PageFactory::class, [], [], '', false); $this->object = $objectManager->getObject( - 'Magento\Cms\Helper\Page', + \Magento\Cms\Helper\Page::class, [ 'context' => $context, 'pageFactory' => $this->pageFactoryMock, @@ -318,7 +326,8 @@ public function testPrepareResultPage( 'cms_page_render', [ 'page' => $this->pageMock, - 'controller_action' => $this->actionMock + 'controller_action' => $this->actionMock, + 'request' => $this->httpRequestMock, ] ); $this->pageMock->expects($this->any()) diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index b5dcd3c18e6ee..d888a037414ba 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -34,9 +34,21 @@ class Topmenu extends Template implements IdentityInterface * Core registry * * @var Registry + * + * @deprecated The property can be removed in a future release. */ protected $registry; + /** + * @var NodeFactory + */ + private $nodeFactory; + + /** + * @var TreeFactory + */ + private $treeFactory; + /** * @param Template\Context $context * @param NodeFactory $nodeFactory @@ -50,13 +62,9 @@ public function __construct( array $data = [] ) { parent::__construct($context, $data); - $this->_menu = $nodeFactory->create( - [ - 'data' => [], - 'idField' => 'root', - 'tree' => $treeFactory->create() - ] - ); + + $this->nodeFactory = $nodeFactory; + $this->treeFactory = $treeFactory; } /** @@ -81,18 +89,18 @@ public function getHtml($outermostClass = '', $childrenWrapClass = '', $limit = { $this->_eventManager->dispatch( 'page_block_html_topmenu_gethtml_before', - ['menu' => $this->_menu, 'block' => $this] + ['menu' => $this->getMenu(), 'block' => $this, 'request' => $this->getRequest()] ); - $this->_menu->setOutermostClass($outermostClass); - $this->_menu->setChildrenWrapClass($childrenWrapClass); + $this->getMenu()->setOutermostClass($outermostClass); + $this->getMenu()->setChildrenWrapClass($childrenWrapClass); - $html = $this->_getHtml($this->_menu, $childrenWrapClass, $limit); + $html = $this->_getHtml($this->getMenu(), $childrenWrapClass, $limit); $transportObject = new \Magento\Framework\DataObject(['html' => $html]); $this->_eventManager->dispatch( 'page_block_html_topmenu_gethtml_after', - ['menu' => $this->_menu, 'transportObject' => $transportObject] + ['menu' => $this->getMenu(), 'transportObject' => $transportObject] ); $html = $transportObject->getHtml(); return $html; @@ -238,13 +246,13 @@ protected function _getHtml( $html .= '
  • _getRenderedMenuItemAttributes($child) . '>'; $html .= '' . $this->escapeHtml( - $child->getName() - ) . '' . $this->_addSubMenu( - $child, - $childLevel, - $childrenWrapClass, - $limit - ) . '
  • '; + $child->getName() + ) . '' . $this->_addSubMenu( + $child, + $childLevel, + $childrenWrapClass, + $limit + ) . ''; $itemPosition++; $counter++; } @@ -370,10 +378,22 @@ protected function getCacheTags() /** * Get menu object. * + * Creates \Magento\Framework\Data\Tree\Node root node object. + * The creation logic was moved from class constructor into separate method. + * * @return Node */ public function getMenu() { + if (!$this->_menu) { + $this->_menu = $this->nodeFactory->create( + [ + 'data' => [], + 'idField' => 'root', + 'tree' => $this->treeFactory->create() + ] + ); + } return $this->_menu; } -} +} \ No newline at end of file diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index 1457dcf4046dc..c0d68efbbd626 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -12,6 +12,11 @@ use Magento\Framework\Data\TreeFactory; use Magento\Framework\Data\Tree\NodeFactory; +/** + * Tests Magento\Theme\Block\Html\Topmenu Class + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TopmenuTest extends \PHPUnit_Framework_TestCase { /** @@ -44,6 +49,21 @@ class TopmenuTest extends \PHPUnit_Framework_TestCase */ protected $category; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $storeManager; + + /** + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + // @codingStandardsIgnoreStart /** @var string */ @@ -56,26 +76,39 @@ class TopmenuTest extends \PHPUnit_Framework_TestCase HTML; - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeManager; - // @codingStandardsIgnoreEnd protected function setUp() { - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class)->getMockForAbstractClass(); + $this->urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) + ->getMockForAbstractClass(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->getMockForAbstractClass(); + + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->getMockForAbstractClass(); + + $this->nodeFactory = $this->getMockBuilder(\Magento\Framework\Data\Tree\NodeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->treeFactory = $this->getMockBuilder(\Magento\Framework\Data\TreeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->context = $objectManager->getObject( - 'Magento\Framework\View\Element\Template\Context', - ['urlBuilder' => $this->urlBuilder, 'storeManager' => $this->storeManager] + \Magento\Framework\View\Element\Template\Context::class, + [ + 'urlBuilder' => $this->urlBuilder, + 'storeManager' => $this->storeManager, + 'eventManager' => $this->eventManagerMock, + 'request' => $this->requestMock, + ] ); - } protected function getTopmenu() @@ -85,25 +118,79 @@ protected function getTopmenu() public function testGetHtmlWithoutSelectedCategory() { - $this->buildTree(false); - $this->assertEquals($this->htmlWithoutCategory, $this->getTopmenu()->getHtml()); + $topmenuBlock = $this->getTopmenu(); + + $treeNode = $this->buildTree(false); + + $transportObject = new \Magento\Framework\DataObject(['html' => $this->htmlWithoutCategory]); + + $this->eventManagerMock->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnMap([ + [ + 'page_block_html_topmenu_gethtml_before', + [ + 'menu' => $treeNode, + 'block' => $topmenuBlock, + 'request' => $this->requestMock, + ], + $this->eventManagerMock + ], + [ + 'page_block_html_topmenu_gethtml_after', + [ + 'menu' => $treeNode, + 'transportObject' => $transportObject, + ], + $this->eventManagerMock + ], + ]); + + $this->assertEquals($this->htmlWithoutCategory, $topmenuBlock->getHtml()); } public function testGetHtmlWithSelectedCategory() { - $this->buildTree(true); - $this->assertEquals($this->htmlWithCategory, $this->getTopmenu()->getHtml()); + $topmenuBlock = $this->getTopmenu(); + + $treeNode = $this->buildTree(true); + + $transportObject = new \Magento\Framework\DataObject(['html' => $this->htmlWithCategory]); + + $this->eventManagerMock->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnMap([ + [ + 'page_block_html_topmenu_gethtml_before', + [ + 'menu' => $treeNode, + 'block' => $topmenuBlock, + 'request' => $this->requestMock, + ], + $this->eventManagerMock + ], + [ + 'page_block_html_topmenu_gethtml_after', + [ + 'menu' => $treeNode, + 'transportObject' => $transportObject, + ], + $this->eventManagerMock + ], + ]); + + $this->assertEquals($this->htmlWithCategory, $topmenuBlock->getHtml()); } public function testGetCacheKeyInfo() { - $nodeFactory = $this->getMock('Magento\Framework\Data\Tree\NodeFactory', [], [], '', false); - $treeFactory = $this->getMock('Magento\Framework\Data\TreeFactory', [], [], '', false); + $nodeFactory = $this->getMock(\Magento\Framework\Data\Tree\NodeFactory::class, [], [], '', false); + $treeFactory = $this->getMock(\Magento\Framework\Data\TreeFactory::class, [], [], '', false); $topmenu = new Topmenu($this->context, $nodeFactory, $treeFactory); $this->urlBuilder->expects($this->once())->method('getUrl')->with('*/*/*')->willReturn('123'); $this->urlBuilder->expects($this->once())->method('getBaseUrl')->willReturn('baseUrl'); - $store = $this->getMockBuilder('Magento\Store\Model\Store') + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->setMethods(['getCode']) ->getMock(); @@ -117,27 +204,36 @@ public function testGetCacheKeyInfo() } /** - * @param $isCurrentItem - * @return void + * Create Tree Node mock object + * + * Helper method, that provides unified logic of creation of Tree Node mock objects. + * + * @param bool $isCurrentItem + * @return \PHPUnit_Framework_MockObject_MockObject */ private function buildTree($isCurrentItem) { - $this->nodeFactory = $this->getMock('Magento\Framework\Data\Tree\NodeFactory', [], [], '', false); - $this->treeFactory = $this->getMock('Magento\Framework\Data\TreeFactory', [], [], '', false); - - $tree = $this->getMock('Magento\Framework\Data\Tree', [], [], '', false); + $treeMock = $this->getMockBuilder(\Magento\Framework\Data\Tree::class) + ->disableOriginalConstructor() + ->getMock(); - $container = $this->getMock('Magento\Catalog\Model\ResourceModel\Category\Tree', [], [], '', false); + $container = $this->getMock(\Magento\Catalog\Model\ResourceModel\Category\Tree::class, [], [], '', false); $children = $this->getMock( - 'Magento\Framework\Data\Tree\Node\Collection', + \Magento\Framework\Data\Tree\Node\Collection::class, ['count'], ['container' => $container] ); for ($i = 0; $i < 10; $i++) { $id = "category-node-$i"; - $categoryNode = $this->getMock('Magento\Framework\Data\Tree\Node', ['getId', 'hasChildren'], [], '', false); + $categoryNode = $this->getMock( + \Magento\Framework\Data\Tree\Node::class, + ['getId', 'hasChildren'], + [], + '', + false + ); $categoryNode->expects($this->once())->method('getId')->willReturn($id); $categoryNode->expects($this->atLeastOnce())->method('hasChildren')->willReturn(false); $categoryNode->setData( @@ -155,12 +251,65 @@ private function buildTree($isCurrentItem) $children->expects($this->once())->method('count')->willReturn(10); - $node = $this->getMock('Magento\Framework\Data\Tree\Node', ['getChildren'], [], '', false); - $node->expects($this->once())->method('getChildren')->willReturn($children); - $node->expects($this->any())->method('__call')->with('getLevel', [])->willReturn(null); + $nodeMock = $this->getMockBuilder(\Magento\Framework\Data\Tree\Node::class) + ->disableOriginalConstructor() + ->setMethods(['getChildren']) + ->getMock(); + $nodeMock->expects($this->once()) + ->method('getChildren') + ->willReturn($children); + $nodeMock->expects($this->any()) + ->method('__call') + ->with('getLevel', []) + ->willReturn(null); + + $nodeMockData = [ + 'data' => [], + 'idField' => 'root', + 'tree' => $treeMock, + ]; + + $this->nodeFactory->expects($this->any()) + ->method('create') + ->with($nodeMockData) + ->willReturn($nodeMock); + + $this->treeFactory->expects($this->once()) + ->method('create') + ->willReturn($treeMock); + + return $nodeMock; + } + + /** + * This tests ensures that getMenu object is equal \Magento\Framework\Data\Tree\Node root node object. + */ + public function testGetMenu() + { + $treeMock = $this->getMockBuilder(\Magento\Framework\Data\Tree::class) + ->disableOriginalConstructor() + ->getMock(); + + $nodeMockData = [ + 'data' => [], + 'idField' => 'root', + 'tree' => $treeMock, + ]; + + $nodeMock = $this->getMockBuilder(\Magento\Framework\Data\Tree\Node::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->nodeFactory->expects($this->any()) + ->method('create') + ->with($nodeMockData) + ->willReturn($nodeMock); - $this->nodeFactory->expects($this->once())->method('create')->willReturn($node); + $this->treeFactory->expects($this->once()) + ->method('create') + ->willReturn($treeMock); - $this->treeFactory->expects($this->once())->method('create')->willReturn($tree); + $topmenuBlock = $this->getTopmenu(); + $this->assertEquals($nodeMock, $topmenuBlock->getMenu()); } -} +} \ No newline at end of file diff --git a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/IndexTest.php b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/IndexTest.php index f25f465549b7e..76b0ff38f1448 100644 --- a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/IndexTest.php +++ b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/IndexTest.php @@ -16,21 +16,23 @@ class IndexTest extends \Magento\Theme\Test\Unit\Controller\Adminhtml\System\Des public function testIndexAction() { $menuModel = $this->getMock( - 'Magento\Backend\Model\Menu', + \Magento\Backend\Model\Menu::class, [], - [$this->getMock('Psr\Log\LoggerInterface')] + [$this->getMock(\Psr\Log\LoggerInterface::class)], + '', + false ); $menuModel->expects($this->once()) ->method('getParentItems') ->with($this->equalTo('Magento_Theme::system_design_theme')) ->will($this->returnValue([])); - $menuBlock = $this->getMock('\Magento\Backend\Block\Menu', [], [], '', false); + $menuBlock = $this->getMock(\Magento\Backend\Block\Menu::class, [], [], '', false); $menuBlock->expects($this->once()) ->method('getMenuModel') ->will($this->returnValue($menuModel)); - $layout = $this->getMock('\Magento\Framework\View\LayoutInterface', [], [], '', false); + $layout = $this->getMock(\Magento\Framework\View\LayoutInterface::class, [], [], '', false); $layout->expects($this->any()) ->method('getBlock') ->with($this->equalTo('menu')) From 4804dd835de31ab0616cf5300ede058942c552e4 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 15:52:15 +0300 Subject: [PATCH 137/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php index 8f68a7e4c2ecb..9b21387d4dc04 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php @@ -7,7 +7,7 @@ namespace Magento\Cms\Test\Unit\Controller; /** - * Tests Magento\Cms\Test\Unit\Controller\Router Class + * Tests Magento\Cms\Controller\Router Class * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ From 287fcff7f0773e892d4dffa06a6ad9040b2c11fd Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Mon, 29 May 2017 16:15:12 +0300 Subject: [PATCH 138/363] MAGETWO-63665: [Backport] - Related Products Rules can't be defined based on product attribute - for 2.1 --- .../Condition/Product/AbstractProduct.php | 4 +- .../Product/Attribute/Edit/AttributeForm.php | 4 +- .../Handler/CatalogProductAttribute/Curl.php | 4 ++ .../Test/Repository/ConfigurableProduct.xml | 34 +++++++++++++++++ .../ConfigurableAttributesData.xml | 38 +++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index c738aa172978d..3879b3cabcc6a 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -602,7 +602,9 @@ public function getBindArgumentValue() */ public function getMappedSqlField() { - if (!$this->isAttributeSetOrCategory()) { + if ($this->getAttribute() == 'sku') { + $mappedSqlField = 'e.sku'; + } elseif (!$this->isAttributeSetOrCategory()) { $mappedSqlField = $this->getEavAttributeTableAlias() . '.value'; } elseif ($this->getAttribute() == 'category_ids') { $mappedSqlField = 'e.entity_id'; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/Edit/AttributeForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/Edit/AttributeForm.php index 20e18bea5a78c..3d06954042984 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/Edit/AttributeForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/Edit/AttributeForm.php @@ -109,7 +109,9 @@ protected function expandAllToggles() { $closedToggles = $this->_rootElement->getElements($this->closedToggle); foreach ($closedToggles as $toggle) { - $toggle->click(); + if ($toggle->isVisible()) { + $toggle->click(); + } } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php index d0e27b203ceec..e00e18fd8ab36 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php @@ -47,6 +47,10 @@ class Curl extends AbstractCurl implements CatalogProductAttributeInterface 'No' => 0, 'Yes' => 1, ], + 'is_global' => [ + 'Store View' => '0', + 'Global' => '1', + ], 'used_in_product_listing' => [ 'No' => '0', 'Yes' => '1', diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml index b57afacd2c308..75d409c640721 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml @@ -176,6 +176,40 @@
    + + Test configurable product with color and size %isolation% + sku_test_configurable_product_%isolation% + This item has weight + 30 + Yes + Catalog, Search + + taxable_goods + + configurable-product-%isolation% + + color_for_promo_rules + + + In Stock + + + + default + + + + default + + + configurable_default + + + 40 + price_40 + + + Test configurable product %isolation% sku_test_configurable_product_%isolation% diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml index 09899f446a76d..04cdb78341ba2 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/ConfigurableAttributesData.xml @@ -421,6 +421,44 @@
    + + + + + + 5.00 + Yes + + + 10.00 + Yes + + + 15.00 + Yes + + + + + + catalogProductAttribute::color_for_promo_rules + + + + 1 + 1 + + + 1 + 1 + + + 1 + 1 + + + + From e046e8fcbdf9d78d36543340351c23c566dc590f Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Mon, 29 May 2017 17:34:41 +0300 Subject: [PATCH 139/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- app/code/Magento/Tax/Model/Config.php | 30 +++ .../Magento/Tax/Model/Config/Notification.php | 6 +- .../Notification/ApplyDiscountOnPrices.php | 143 ++++++++++++ .../Message/Notification/DiscountErrors.php | 144 ++++++++++++ .../Message/Notification/RoundingErrors.php | 151 +++++++++++++ .../System/Message/NotificationInterface.php | 13 ++ .../Model/System/Message/Notifications.php | 208 ++++++++---------- .../Tax/Test/Unit/Model/ConfigTest.php | 6 + .../ApplyDiscountOnPricesTest.php | 160 ++++++++++++++ .../Notification/DiscountErrorsTest.php | 103 +++++++++ .../Notification/RoundingErrorsTest.php | 142 ++++++++++++ .../System/Message/NotificationsTest.php | 97 ++++++++ app/code/Magento/Tax/etc/adminhtml/di.xml | 9 + app/code/Magento/Tax/etc/adminhtml/system.xml | 2 +- .../Adminhtml/System/Config/Notification.php | 34 +++ .../System/Config/NotificationPopup.php | 36 +++ .../Block/Adminhtml/System/Config/Tax.php | 74 +++++++ ...ertTaxConfigurationNotificationMessage.php | 50 +++++ ...sertTaxConfigurationSuccessSaveMessage.php | 44 ++++ .../Test/Page/Adminhtml/TaxConfiguration.xml | 16 ++ .../Test/TestCase/TaxConfigurationTest.php | 61 +++++ .../Test/TestCase/TaxConfigurationTest.xml | 18 ++ .../tests/app/Magento/Tax/Test/etc/di.xml | 10 + .../InjectableTests/MAGETWO-61131.xml | 15 ++ 24 files changed, 1455 insertions(+), 117 deletions(-) create mode 100644 app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php create mode 100644 app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php create mode 100644 app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php create mode 100644 app/code/Magento/Tax/Model/System/Message/NotificationInterface.php create mode 100644 app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php create mode 100644 app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php create mode 100644 app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php create mode 100644 app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php index 2baad1caac831..d8d510a11239f 100644 --- a/app/code/Magento/Tax/Model/Config.php +++ b/app/code/Magento/Tax/Model/Config.php @@ -23,6 +23,8 @@ class Config const XML_PATH_TAX_NOTIFICATION_IGNORE_PRICE_DISPLAY = 'tax/notification/ignore_price_display'; + const XML_PATH_TAX_NOTIFICATION_IGNORE_APPLY_DISCOUNT = 'tax/notification/ignore_apply_discount'; + const XML_PATH_TAX_NOTIFICATION_INFO_URL = 'tax/notification/info_url'; // tax classes @@ -68,6 +70,7 @@ class Config const XML_PATH_DISPLAY_CART_SHIPPING = 'tax/cart_display/shipping'; + /** @deprecated */ const XML_PATH_DISPLAY_CART_DISCOUNT = 'tax/cart_display/discount'; const XML_PATH_DISPLAY_CART_GRANDTOTAL = 'tax/cart_display/grandtotal'; @@ -85,6 +88,7 @@ class Config const XML_PATH_DISPLAY_SALES_SHIPPING = 'tax/sales_display/shipping'; + /** @deprecated */ const XML_PATH_DISPLAY_SALES_DISCOUNT = 'tax/sales_display/discount'; const XML_PATH_DISPLAY_SALES_GRANDTOTAL = 'tax/sales_display/grandtotal'; @@ -468,6 +472,7 @@ public function displayCartShippingBoth($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displayCartDiscountInclTax($store = null) { @@ -481,6 +486,7 @@ public function displayCartDiscountInclTax($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displayCartDiscountExclTax($store = null) { @@ -494,6 +500,7 @@ public function displayCartDiscountExclTax($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displayCartDiscountBoth($store = null) { @@ -663,6 +670,7 @@ public function displaySalesShippingBoth($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displaySalesDiscountInclTax($store = null) { @@ -676,6 +684,7 @@ public function displaySalesDiscountInclTax($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displaySalesDiscountExclTax($store = null) { @@ -689,6 +698,7 @@ public function displaySalesDiscountExclTax($store = null) /** * @param null|string|bool|int|Store $store * @return bool + * @deprecated */ public function displaySalesDiscountBoth($store = null) { @@ -753,6 +763,26 @@ public function crossBorderTradeEnabled($store = null) ); } + /* + * Check if admin notification related to misconfiguration of "Apply Discount On Prices" should be ignored. + * + * Warning is displayed in case when "Catalog Prices" = "Excluding Tax" + * AND "Apply Discount On Prices" = "Including Tax" + * AND "Apply Customer Tax" = "After Discount" + * + * @param null|string|Store $store + * @return bool + */ + public function isWrongApplyDiscountSettingIgnored($store = null) + { + return (bool)$this->_scopeConfig->getValue( + self::XML_PATH_TAX_NOTIFICATION_IGNORE_APPLY_DISCOUNT, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); + } + + /** * Check if do not show notification about wrong display settings * diff --git a/app/code/Magento/Tax/Model/Config/Notification.php b/app/code/Magento/Tax/Model/Config/Notification.php index 135f0aafb237b..cc2c17a7276fa 100644 --- a/app/code/Magento/Tax/Model/Config/Notification.php +++ b/app/code/Magento/Tax/Model/Config/Notification.php @@ -6,6 +6,7 @@ namespace Magento\Tax\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Tax\Model\Config; /** * Tax Config Notification @@ -49,8 +50,9 @@ public function __construct( public function afterSave() { if ($this->isValueChanged()) { - $this->_resetNotificationFlag(\Magento\Tax\Model\Config::XML_PATH_TAX_NOTIFICATION_IGNORE_DISCOUNT); - $this->_resetNotificationFlag(\Magento\Tax\Model\Config::XML_PATH_TAX_NOTIFICATION_IGNORE_PRICE_DISPLAY); + $this->_resetNotificationFlag(Config::XML_PATH_TAX_NOTIFICATION_IGNORE_DISCOUNT); + $this->_resetNotificationFlag(Config::XML_PATH_TAX_NOTIFICATION_IGNORE_PRICE_DISPLAY); + $this->_resetNotificationFlag(Config::XML_PATH_TAX_NOTIFICATION_IGNORE_APPLY_DISCOUNT); } return parent::afterSave(); } diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php new file mode 100644 index 0000000000000..4e5497dbdff2b --- /dev/null +++ b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php @@ -0,0 +1,143 @@ +storeManager = $storeManager; + $this->urlBuilder = $urlBuilder; + $this->taxConfig = $taxConfig; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getIdentity() + { + return 'TAX_NOTIFICATION_APPLY_DISCOUNT'; + } + + /** + * {@inheritdoc} + */ + public function isDisplayed() + { + if (!$this->taxConfig->isWrongApplyDiscountSettingIgnored() && $this->getStoresWithWrongSettings()) { + return true; + } + return false; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + $messageDetails = ''; + + if ($this->isDisplayed()) { + $messageDetails .= ''; + $messageDetails .= __('To apply the discount on prices including tax and apply the tax after discount,'. + ' set Catalog Prices to “Including Tax”. '); + $messageDetails .= '

    '; + $messageDetails .= __('Store(s) affected: '); + $messageDetails .= implode(', ', $this->getStoresWithWrongSettings()); + $messageDetails .= '

    '; + $messageDetails .= __( + 'Click on the link to ignore this notification', + $this->urlBuilder->getUrl('tax/tax/ignoreTaxNotification', ['section' => 'apply_discount']) + ); + $messageDetails .= "

    "; + } + + return $messageDetails; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getSeverity() + { + return self::SEVERITY_CRITICAL; + } + + /** + * Return list of store names which have invalid settings. + * + * @return array + */ + private function getStoresWithWrongSettings() + { + if (null !== $this->storesWithInvalidSettings) { + return $this->storesWithInvalidSettings; + } + $this->storesWithInvalidSettings = []; + $storeCollection = $this->storeManager->getStores(true); + foreach ($storeCollection as $store) { + if (!$this->checkSettings($store)) { + $website = $store->getWebsite(); + $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; + } + } + return $this->storesWithInvalidSettings; + } + + /** + * Check if settings are valid. + * + * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @return bool false if settings are incorrect + */ + private function checkSettings($store = null) + { + return $this->taxConfig->priceIncludesTax($store) + || !$this->taxConfig->applyTaxAfterDiscount($store) + || !$this->taxConfig->discountTax($store); + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php new file mode 100644 index 0000000000000..dd36a40d1c5a2 --- /dev/null +++ b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php @@ -0,0 +1,144 @@ +storeManager = $storeManager; + $this->urlBuilder = $urlBuilder; + $this->taxConfig = $taxConfig; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getIdentity() + { + return 'TAX_NOTIFICATION_DISCOUNT_ERRORS'; + } + + /** + * {@inheritdoc} + */ + public function isDisplayed() + { + if (!$this->taxConfig->isWrongDiscountSettingsIgnored() && $this->getStoresWithWrongSettings()) { + return true; + } + return false; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + $messageDetails = ''; + + if (!empty($this->getStoresWithWrongSettings()) && !$this->taxConfig->isWrongDiscountSettingsIgnored()) { + $messageDetails .= ''; + $messageDetails .= __('With customer tax applied “Before Discount”,' . + ' the final discount calculation may not match customers’ expectations.'); + $messageDetails .= '

    '; + $messageDetails .= __('Store(s) affected: '); + $messageDetails .= implode(', ', $this->getStoresWithWrongSettings()); + $messageDetails .= '

    '; + $messageDetails .= __( + 'Click on the link to ignore this notification', + $this->urlBuilder->getUrl('tax/tax/ignoreTaxNotification', ['section' => 'discount']) + ); + $messageDetails .= "

    "; + } + + return $messageDetails; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getSeverity() + { + return self::SEVERITY_CRITICAL; + } + + /** + * Check if tax discount settings are compatible + * + * Matrix for invalid discount settings is as follows: + * Before Discount / Excluding Tax + * Before Discount / Including Tax + * + * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @return bool + */ + private function checkSettings($store = null) + { + return $this->taxConfig->applyTaxAfterDiscount($store); + } + + /** + * Return list of store names where tax discount settings are compatible. + * Return true if settings are wrong for default store. + * + * @return array + */ + private function getStoresWithWrongSettings() + { + if (null !== $this->storesWithInvalidSettings) { + return $this->storesWithInvalidSettings; + } + $this->storesWithInvalidSettings = []; + $storeCollection = $this->storeManager->getStores(true); + foreach ($storeCollection as $store) { + if (!$this->checkSettings($store)) { + $website = $store->getWebsite(); + $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; + } + } + return $this->storesWithInvalidSettings; + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php new file mode 100644 index 0000000000000..f32cde63eb08e --- /dev/null +++ b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php @@ -0,0 +1,151 @@ +storeManager = $storeManager; + $this->urlBuilder = $urlBuilder; + $this->taxConfig = $taxConfig; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getIdentity() + { + return 'TAX_NOTIFICATION_ROUNDING_ERRORS'; + } + + /** + * {@inheritdoc} + */ + public function isDisplayed() + { + if (!$this->taxConfig->isWrongDisplaySettingsIgnored() && $this->getStoresWithWrongSettings()) { + return true; + } + return false; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + $messageDetails = ''; + + if (!empty($this->getStoresWithWrongSettings()) && !$this->taxConfig->isWrongDisplaySettingsIgnored()) { + $messageDetails .= ''; + $messageDetails .= __('Your current tax configuration may result in rounding errors. '); + $messageDetails .= '

    '; + $messageDetails .= __('Store(s) affected: '); + $messageDetails .= implode(', ', $this->getStoresWithWrongSettings()); + $messageDetails .= '

    '; + $messageDetails .= __( + 'Click on the link to ignore this notification', + $this->urlBuilder->getUrl('tax/tax/ignoreTaxNotification', ['section' => 'price_display']) + ); + $messageDetails .= "

    "; + } + + return $messageDetails; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function getSeverity() + { + return self::SEVERITY_CRITICAL; + } + + /** + * Check if tax calculation type and price display settings are compatible. + * + * Invalid settings if + * Tax Calculation Method Based On 'Total' or 'Row' + * and at least one Price Display Settings has 'Including and Excluding Tax' value. + * + * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @return bool + */ + private function checkSettings($store = null) + { + if ($this->taxConfig->getAlgorithm($store) == \Magento\Tax\Model\Calculation::CALC_UNIT_BASE) { + return true; + } + return $this->taxConfig->getPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH + && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH + && !$this->taxConfig->displayCartPricesBoth($store) + && !$this->taxConfig->displayCartSubtotalBoth($store) + && !$this->taxConfig->displayCartShippingBoth($store) + && !$this->taxConfig->displaySalesPricesBoth($store) + && !$this->taxConfig->displaySalesSubtotalBoth($store) + && !$this->taxConfig->displaySalesShippingBoth($store); + } + + /** + * Return list of store names which have not compatible tax calculation type and price display settings. + * Return true if settings are wrong for default store. + * + * @return array + */ + private function getStoresWithWrongSettings() + { + if (null !== $this->storesWithInvalidSettings) { + return $this->storesWithInvalidSettings; + } + $this->storesWithInvalidSettings = []; + $storeCollection = $this->storeManager->getStores(true); + foreach ($storeCollection as $store) { + if (!$this->checkSettings($store)) { + $website = $store->getWebsite(); + $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; + } + } + return $this->storesWithInvalidSettings; + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php b/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php new file mode 100644 index 0000000000000..3bb309740cfb1 --- /dev/null +++ b/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php @@ -0,0 +1,13 @@ +storeManager = $storeManager; $this->urlBuilder = $urlBuilder; $this->taxConfig = $taxConfig; + $this->notifications = $notifications; } /** * Retrieve unique message identity * * @return string + * @codeCoverageIgnore */ public function getIdentity() { return md5('TAX_NOTIFICATION'); } + /** + * {@inheritdoc} + */ + public function isDisplayed() + { + foreach ($this->notifications as $notification) { + if ($notification->isDisplayed()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + $messageDetails = ''; + + foreach ($this->notifications as $notification) { + $messageDetails .= $notification->getText(); + } + + $messageDetails .= '

    '; + $messageDetails .= __('Please see documentation for more details. ', $this->getInfoUrl()); + $messageDetails .= __( + 'Click here to go to Tax Configuration and change your settings.', + $this->getManageUrl() + ); + $messageDetails .= '

    '; + + return $messageDetails; + } + + /** + * Retrieve message severity + * + * @return int + * @codeCoverageIgnore + */ + public function getSeverity() + { + return self::SEVERITY_CRITICAL; + } + + /** + * Get URL for the tax notification documentation + * + * @return string + */ + public function getInfoUrl() + { + return $this->taxConfig->getInfoUrl(); + } + + /** + * Get URL to the admin tax configuration page + * + * @return string + */ + public function getManageUrl() + { + return $this->urlBuilder->getUrl('adminhtml/system_config/edit/section/tax'); + } + /** * Check if tax calculation type and price display settings are compatible * @@ -77,6 +158,8 @@ public function getIdentity() * * @param null|int|bool|string|\Magento\Store\Model\Store $store $store * @return bool + * @deprecated + * @see \Magento\Tax\Model\System\Message\Notification\RoundingErrors::checkSettings */ public function checkDisplaySettings($store = null) { @@ -102,37 +185,20 @@ public function checkDisplaySettings($store = null) * * @param null|int|bool|string|\Magento\Store\Model\Store $store $store * @return bool + * @deprecated + * @see \Magento\Tax\Model\System\Message\Notification\DiscountErrors::checkSettings */ public function checkDiscountSettings($store = null) { return $this->taxConfig->applyTaxAfterDiscount($store); } - /** - * Get URL for the tax notification documentation - * - * @return string - */ - public function getInfoUrl() - { - return $this->taxConfig->getInfoUrl(); - } - - /** - * Get URL to the admin tax configuration page - * - * @return string - */ - public function getManageUrl() - { - return $this->urlBuilder->getUrl('adminhtml/system_config/edit/section/tax'); - } - /** * Get URL to ignore tax notifications * * @param string $section * @return string + * @deprecated */ public function getIgnoreTaxNotificationUrl($section) { @@ -144,6 +210,8 @@ public function getIgnoreTaxNotificationUrl($section) * Return true if settings are wrong for default store. * * @return array + * @deprecated + * @see \Magento\Tax\Model\System\Message\Notification\RoundingErrors::getStoresWithWrongSettings */ public function getStoresWithWrongDisplaySettings() { @@ -163,6 +231,8 @@ public function getStoresWithWrongDisplaySettings() * Return true if settings are wrong for default store. * * @return array + * @deprecated + * @see \Magento\Tax\Model\System\Message\Notification\DiscountErrors::getStoresWithWrongSettings */ public function getStoresWithWrongDiscountSettings() { @@ -176,94 +246,4 @@ public function getStoresWithWrongDiscountSettings() } return $storeNames; } - - /** - * Check whether notification is displayed - * Checks if any of these settings are being ignored or valid: - * 1. Wrong discount settings - * 2. Wrong display settings - * - * @return bool - */ - public function isDisplayed() - { - // Check if we are ignoring all notifications - if ($this->taxConfig->isWrongDisplaySettingsIgnored() && $this->taxConfig->isWrongDiscountSettingsIgnored()) { - return false; - } - - $this->storesWithInvalidDisplaySettings = $this->getStoresWithWrongDisplaySettings(); - $this->storesWithInvalidDiscountSettings = $this->getStoresWithWrongDiscountSettings(); - - // Check if we have valid tax notifications - if ((!empty($this->storesWithInvalidDisplaySettings) && !$this->taxConfig->isWrongDisplaySettingsIgnored()) - || (!empty($this->storesWithInvalidDiscountSettings) && !$this->taxConfig->isWrongDiscountSettingsIgnored()) - ) { - return true; - } - - return false; - } - - /** - * Build message text - * Determine which notification and data to display - * - * @return string - */ - public function getText() - { - $messageDetails = ''; - - if (!empty($this->storesWithInvalidDisplaySettings) && !$this->taxConfig->isWrongDisplaySettingsIgnored()) { - $messageDetails .= ''; - $messageDetails .= __('Warning tax configuration can result in rounding errors. '); - $messageDetails .= '

    '; - $messageDetails .= __('Store(s) affected: '); - $messageDetails .= implode(', ', $this->storesWithInvalidDisplaySettings); - $messageDetails .= '

    '; - $messageDetails .= __( - 'Click on the link to ignore this notification', - $this->getIgnoreTaxNotificationUrl('price_display') - ); - $messageDetails .= "

    "; - } - - if (!empty($this->storesWithInvalidDiscountSettings) && !$this->taxConfig->isWrongDiscountSettingsIgnored()) { - $messageDetails .= ''; - $messageDetails .= __( - 'Warning tax discount configuration might result in different discounts - than a customer might expect. ' - ); - $messageDetails .= '

    '; - $messageDetails .= __('Store(s) affected: '); - $messageDetails .= implode(', ', $this->storesWithInvalidDiscountSettings); - $messageDetails .= '

    '; - $messageDetails .= __( - 'Click on the link to ignore this notification', - $this->getIgnoreTaxNotificationUrl('discount') - ); - $messageDetails .= "

    "; - } - - $messageDetails .= '

    '; - $messageDetails .= __('Please see documentation for more details. ', $this->getInfoUrl()); - $messageDetails .= __( - 'Click here to go to Tax Configuration and change your settings.', - $this->getManageUrl() - ); - $messageDetails .= '

    '; - - return $messageDetails; - } - - /** - * Retrieve message severity - * - * @return int - */ - public function getSeverity() - { - return self::SEVERITY_CRITICAL; - } -} +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Tax/Test/Unit/Model/ConfigTest.php index a37426d69c957..4dfbba94bae73 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/ConfigTest.php @@ -360,6 +360,12 @@ public function dataProviderScopeConfigMethods() true, true ], + [ + 'isWrongApplyDiscountSettingIgnored', + Config::XML_PATH_TAX_NOTIFICATION_IGNORE_APPLY_DISCOUNT, + true, + true + ], [ 'getInfoUrl', Config::XML_PATH_TAX_NOTIFICATION_INFO_URL, diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php new file mode 100644 index 0000000000000..8baf6e421a3d5 --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php @@ -0,0 +1,160 @@ +getMock(WebsiteInterface::class, [], [], '', false); + $websiteMock->expects($this->any())->method('getName')->willReturn('testWebsiteName'); + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + false, + true, + true, + ['getWebsite', 'getName'] + ); + $storeMock->expects($this->any())->method('getName')->willReturn('testStoreName'); + $storeMock->expects($this->any())->method('getWebsite')->willReturn($websiteMock); + $this->storeManagerMock = $this->getMock(StoreManagerInterface::class, [], [], '', false); + $this->storeManagerMock->expects($this->any())->method('getStores')->willReturn([$storeMock]); + + $this->urlBuilderMock = $this->getMock(UrlInterface::class, [], [], '', false); + $this->taxConfigMock = $this->getMock(TaxConfig::class, [], [], '', false); + $this->applyDiscountOnPricesNotification = (new ObjectManager($this))->getObject( + ApplyDiscountOnPricesNotification::class, + [ + 'storeManager' => $this->storeManagerMock, + 'urlBuilder' => $this->urlBuilderMock, + 'taxConfig' => $this->taxConfigMock, + ] + ); + } + + /** + * @dataProvider dataProviderIsDisplayed + */ + public function testIsDisplayed( + $isWrongApplyDiscountSettingIgnored, + $priceIncludesTax, + $applyTaxAfterDiscount, + $discountTax, + $expectedResult + ) { + $this->taxConfigMock->expects($this->any())->method('isWrongApplyDiscountSettingIgnored') + ->willReturn($isWrongApplyDiscountSettingIgnored); + $this->taxConfigMock->expects($this->any())->method('priceIncludesTax')->willReturn($priceIncludesTax); + $this->taxConfigMock->expects($this->any())->method('applyTaxAfterDiscount') + ->willReturn($applyTaxAfterDiscount); + $this->taxConfigMock->expects($this->any())->method('discountTax')->willReturn($discountTax); + + $this->assertEquals($expectedResult, $this->applyDiscountOnPricesNotification->isDisplayed()); + } + + public function dataProviderIsDisplayed() + { + return [ + [ + false, // $isWrongApplyDiscountSettingIgnored, + false, // $priceIncludesTax, + true, // $applyTaxAfterDiscount, + true, // $discountTax, + true // $expectedResult + ], + [ + false, // $isWrongApplyDiscountSettingIgnored, + false, // $priceIncludesTax, + true, // $applyTaxAfterDiscount, + false, // $discountTax, + false // $expectedResult + ], + [ + false, // $isWrongApplyDiscountSettingIgnored, + false, // $priceIncludesTax, + false, // $applyTaxAfterDiscount, + true, // $discountTax, + false // $expectedResult + ], + [ + false, // $isWrongApplyDiscountSettingIgnored, + true, // $priceIncludesTax, + true, // $applyTaxAfterDiscount, + true, // $discountTax, + false // $expectedResult + ], + [ + true, // $isWrongApplyDiscountSettingIgnored, + false, // $priceIncludesTax, + true, // $applyTaxAfterDiscount, + true, // $discountTax, + false // $expectedResult + ] + ]; + } + + public function testGetText() + { + $this->taxConfigMock->expects($this->any())->method('isWrongApplyDiscountSettingIgnored')->willReturn(false); + + $this->taxConfigMock->expects($this->any())->method('priceIncludesTax')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('applyTaxAfterDiscount')->willReturn(true); + $this->taxConfigMock->expects($this->any())->method('discountTax')->willReturn(true); + + $this->urlBuilderMock->expects($this->any()) + ->method('getUrl') + ->with('tax/tax/ignoreTaxNotification', ['section' => 'apply_discount']) + ->willReturn('http://example.com'); + $this->applyDiscountOnPricesNotification->isDisplayed(); + $this->assertEquals( + 'To apply the discount on prices including tax and apply the tax after discount, ' + . 'set Catalog Prices to “Including Tax”.

    Store(s) affected: testWebsiteName ' + . '(testStoreName)

    Click on the link to ' + . 'ignore this notification

    ', + $this->applyDiscountOnPricesNotification->getText() + ); + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php new file mode 100644 index 0000000000000..1901cd543ce53 --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php @@ -0,0 +1,103 @@ +getMock(WebsiteInterface::class, [], [], '', false); + $websiteMock->expects($this->any())->method('getName')->willReturn('testWebsiteName'); + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + false, + true, + true, + ['getWebsite', 'getName'] + ); + $storeMock->expects($this->any())->method('getName')->willReturn('testStoreName'); + $storeMock->expects($this->any())->method('getWebsite')->willReturn($websiteMock); + $this->storeManagerMock = $this->getMock(StoreManagerInterface::class, [], [], '', false); + $this->storeManagerMock->expects($this->any())->method('getStores')->willReturn([$storeMock]); + + $this->urlBuilderMock = $this->getMock(UrlInterface::class, [], [], '', false); + $this->taxConfigMock = $this->getMock(TaxConfig::class, [], [], '', false); + $this->discountErrorsNotification = (new ObjectManager($this))->getObject( + DiscountErrorsNotification::class, + [ + 'storeManager' => $this->storeManagerMock, + 'urlBuilder' => $this->urlBuilderMock, + 'taxConfig' => $this->taxConfigMock, + ] + ); + } + + public function testIsDisplayed() + { + $this->taxConfigMock->expects($this->any())->method('applyTaxAfterDiscount')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('isWrongDiscountSettingsIgnored')->willReturn(false); + $this->assertTrue($this->discountErrorsNotification->isDisplayed()); + } + + public function testIsDisplayedIgnoreWrongConfiguration() + { + $this->taxConfigMock->expects($this->any())->method('applyTaxAfterDiscount')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('isWrongDiscountSettingsIgnored')->willReturn(true); + $this->assertFalse($this->discountErrorsNotification->isDisplayed()); + } + + public function testGetText() + { + $this->taxConfigMock->expects($this->any())->method('applyTaxAfterDiscount')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('isWrongDiscountSettingsIgnored')->willReturn(false); + $this->urlBuilderMock->expects($this->any()) + ->method('getUrl') + ->with('tax/tax/ignoreTaxNotification', ['section' => 'discount']) + ->willReturn('http://example.com'); + $this->discountErrorsNotification->isDisplayed(); + $this->assertEquals( + 'With customer tax applied “Before Discount”, the final discount calculation may not match ' + . 'customers’ expectations.

    Store(s) affected: testWebsiteName (testStoreName)' + . '

    Click on the link to ignore this notification

    ', + $this->discountErrorsNotification->getText() + ); + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php new file mode 100644 index 0000000000000..8568f786aba4c --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php @@ -0,0 +1,142 @@ +getMock(WebsiteInterface::class, [], [], '', false); + $websiteMock->expects($this->any())->method('getName')->willReturn('testWebsiteName'); + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + false, + true, + true, + ['getWebsite', 'getName'] + ); + $storeMock->expects($this->any())->method('getName')->willReturn('testStoreName'); + $storeMock->expects($this->any())->method('getWebsite')->willReturn($websiteMock); + $this->storeManagerMock = $this->getMock(StoreManagerInterface::class, [], [], '', false); + $this->storeManagerMock->expects($this->any())->method('getStores')->willReturn([$storeMock]); + + $this->urlBuilderMock = $this->getMock(UrlInterface::class, [], [], '', false); + $this->taxConfigMock = $this->getMock(TaxConfig::class, [], [], '', false); + $this->roundingErrorsNotification = (new ObjectManager($this))->getObject( + RoundingErrorsNotification::class, + [ + 'storeManager' => $this->storeManagerMock, + 'urlBuilder' => $this->urlBuilderMock, + 'taxConfig' => $this->taxConfigMock, + ] + ); + } + + public function testIsDisplayedNotDisplayedUnitBased() + { + $this->taxConfigMock->expects($this->any())->method('isWrongDisplaySettingsIgnored')->willReturn(false); + + $this->taxConfigMock->expects($this->any()) + ->method('getAlgorithm')->willReturn(\Magento\Tax\Model\Calculation::CALC_UNIT_BASE); + + $this->taxConfigMock->expects($this->any()) + ->method('getPriceDisplayType')->willReturn(\Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX); + $this->taxConfigMock->expects($this->any()) + ->method('getShippingPriceDisplayType')->willReturn(\Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX); + + $this->taxConfigMock->expects($this->any())->method('displayCartPricesBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displayCartSubtotalBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displayCartShippingBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displaySalesPricesBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displaySalesSubtotalBoth')->willReturn(false); + + $this->taxConfigMock->expects($this->any())->method('displaySalesShippingBoth')->willReturn(true); + + $this->assertFalse($this->roundingErrorsNotification->isDisplayed()); + } + + public function testIsDisplayedNotDisplayed() + { + $this->taxConfigMock->expects($this->any())->method('isWrongDisplaySettingsIgnored')->willReturn(false); + + $this->taxConfigMock->expects($this->any()) + ->method('getAlgorithm')->willReturn(\Magento\Tax\Model\Calculation::CALC_ROW_BASE); + + $this->taxConfigMock->expects($this->any()) + ->method('getPriceDisplayType')->willReturn(\Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX); + $this->taxConfigMock->expects($this->any()) + ->method('getShippingPriceDisplayType')->willReturn(\Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX); + + $this->taxConfigMock->expects($this->any())->method('displayCartPricesBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displayCartSubtotalBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displayCartShippingBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displaySalesPricesBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displaySalesSubtotalBoth')->willReturn(false); + $this->taxConfigMock->expects($this->any())->method('displaySalesShippingBoth')->willReturn(false); + + $this->assertFalse($this->roundingErrorsNotification->isDisplayed()); + } + + public function testIsDisplayedIgnoreWrongConfiguration() + { + $this->taxConfigMock->expects($this->any())->method('isWrongDisplaySettingsIgnored')->willReturn(true); + $this->assertFalse($this->roundingErrorsNotification->isDisplayed()); + } + + public function testGetText() + { + $this->taxConfigMock->expects($this->any())->method('isWrongDisplaySettingsIgnored')->willReturn(false); + + $this->taxConfigMock->expects($this->any())->method('displaySalesShippingBoth')->willReturn(true); + + $this->urlBuilderMock->expects($this->any()) + ->method('getUrl') + ->with('tax/tax/ignoreTaxNotification', ['section' => 'price_display']) + ->willReturn('http://example.com'); + $this->roundingErrorsNotification->isDisplayed(); + $this->assertEquals( + 'Your current tax configuration may result in rounding errors. ' + . '

    Store(s) affected: testWebsiteName (testStoreName)

    Click on the link to ' + . 'ignore this notification

    ', + $this->roundingErrorsNotification->getText() + ); + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php new file mode 100644 index 0000000000000..522bd68f90cbb --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php @@ -0,0 +1,97 @@ +storeManagerMock = $this->getMock(StoreManagerInterface::class, [], [], '', false); + $this->urlBuilderMock = $this->getMock(UrlInterface::class, [], [], '', false); + $this->taxConfigMock = $this->getMock(TaxConfig::class, [], [], '', false); + $this->notificationMock = $this->getMock(NotificationInterface::class, [], [], '', false); + $this->notifications = (new ObjectManager($this))->getObject( + Notifications::class, + [ + 'storeManager' => $this->storeManagerMock, + 'urlBuilder' => $this->urlBuilderMock, + 'taxConfig' => $this->taxConfigMock, + 'notifications' => [$this->notificationMock] + ] + ); + } + + /** + * @dataProvider dataProviderIsDisplayed + */ + public function testIsDisplayed( + $isNotificationDisplayed, + $expectedResult + ) { + $this->notificationMock->expects($this->once())->method('isDisplayed')->willReturn($isNotificationDisplayed); + $this->assertEquals($expectedResult, $this->notifications->isDisplayed()); + } + + public function dataProviderIsDisplayed() + { + return [ + [true, true], + [false, false] + ]; + } + + public function testGetText() + { + $this->notificationMock->expects($this->once())->method('getText')->willReturn('Notification Text.'); + $this->taxConfigMock->expects($this->once())->method('getInfoUrl')->willReturn('http://info-url'); + $this->urlBuilderMock->expects($this->once())->method('getUrl') + ->with('adminhtml/system_config/edit/section/tax')->willReturn('http://tax-config-url'); + + $this->assertEquals( + 'Notification Text.

    Please see documentation for more details. ' + . 'Click here to go to Tax Configuration and change your settings.

    ', + $this->notifications->getText() + ); + } +} \ No newline at end of file diff --git a/app/code/Magento/Tax/etc/adminhtml/di.xml b/app/code/Magento/Tax/etc/adminhtml/di.xml index b14c61d5abbef..16cb3a15ae84e 100644 --- a/app/code/Magento/Tax/etc/adminhtml/di.xml +++ b/app/code/Magento/Tax/etc/adminhtml/di.xml @@ -13,4 +13,13 @@ + + + + Magento\Tax\Model\System\Message\Notification\RoundingErrors + Magento\Tax\Model\System\Message\Notification\DiscountErrors + Magento\Tax\Model\System\Message\Notification\ApplyDiscountOnPrices + + + diff --git a/app/code/Magento/Tax/etc/adminhtml/system.xml b/app/code/Magento/Tax/etc/adminhtml/system.xml index 5dedc5da96788..bcc132b6a964b 100644 --- a/app/code/Magento/Tax/etc/adminhtml/system.xml +++ b/app/code/Magento/Tax/etc/adminhtml/system.xml @@ -60,7 +60,7 @@ Magento\Tax\Model\System\Config\Source\PriceType Magento\Tax\Model\Config\Notification - Apply discount on price including tax is calculated based on store tax if "Apply Tax after Discount" is selected. + Warning: To apply the discount on prices including tax and apply the tax after discount, set Catalog Prices to “Including Tax”.
    diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php new file mode 100644 index 0000000000000..004b7cf816c68 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php @@ -0,0 +1,34 @@ +_rootElement->find($this->messageLink); + if ($messageLink->isVisible()) { + $messageLink->click(); + } + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php new file mode 100644 index 0000000000000..0a99c272495e8 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php @@ -0,0 +1,36 @@ +_rootElement->isVisible()) { + $message = $this->_rootElement->find($this->messageSystemList)->getText(); + } + + return $message; + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php new file mode 100644 index 0000000000000..aa0bb30f3446f --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php @@ -0,0 +1,74 @@ +origData = $data; + foreach ($data as $tab => $fields) { + $this->openTab($tab); + foreach ($fields as $id => $value) { + /** @var CheckboxElement $checkbox */ + $checkbox = $this->_rootElement->find('#' . $id . '_inherit', Locator::SELECTOR_CSS, 'checkbox'); + $checkbox->setValue('No'); + /** @var SelectElement $field */ + $field = $this->_rootElement->find('#' . $id, Locator::SELECTOR_CSS, 'select'); + $this->origData[$tab][$id]['value'] = $field->getValue(); + $field->setValue($value['value']); + } + } + } + + /** + * Open tax configuration tab. + * + * @param string $tab + * @return void + */ + public function openTab($tab) + { + $id = '#' . $tab . '-head'; + $element = $this->_rootElement->find($id); + if (!$element->getAttribute('class')) { + $element->click(); + } + } + + /** + * Restore original configuration data. + * + * @return void + */ + public function rollback() + { + if ($this->origData) { + $this->fill($this->origData); + } + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php new file mode 100644 index 0000000000000..0ebe69da95c66 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php @@ -0,0 +1,50 @@ +getNotificationBlockPopup()->isVisible()) { + $taxConfiguration->getNotificationBlock()->openNotificationPopup(); + } + $message = $taxConfiguration->getNotificationBlockPopup()->getNotificationMessage(); + + \PHPUnit_Framework_Assert::assertContains( + self::NOTIFICATION, + $message, + "Notification wasn't displayed." + ); + } + + /** + * Text of Saved Tax Configuration Notification Message assert. + * + * @return string + */ + public function toString() + { + return 'Tax notification message is present.'; + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php new file mode 100644 index 0000000000000..e1d9874f8417c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php @@ -0,0 +1,44 @@ +getMessagesBlock()->getSuccessMessage(); + \PHPUnit_Framework_Assert::assertEquals( + self::SUCCESS_MESSAGE, + $actualMessage, + "Tax configuration was not saved." + ); + } + + /** + * Text of Saved Tax Configuration Success Message assert. + * + * @return string + */ + public function toString() + { + return 'Save tax configuration success message is present.'; + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml new file mode 100644 index 0000000000000..79d31b62e6a15 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php new file mode 100644 index 0000000000000..8ac4a2d0e9390 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php @@ -0,0 +1,61 @@ + Configuration > Sales > Tax. + * 3. Fill Tax configuration according to data set. + * 4. Save configuration. + * 5. Perform all assertions. + * + * @group Tax_(CS) + * @ZephyrId MAGETWO-64653 + */ +class TaxConfigurationTest extends Injectable +{ + /** + * @var TaxConfiguration + */ + private $taxConfig; + + /** + * Injection data. + * + * @param TaxConfiguration $taxConfiguration + * @return void + */ + public function __inject(TaxConfiguration $taxConfiguration) + { + $this->taxConfig = $taxConfiguration; + } + + /** + * Run apply tax configuration test. + * + * @param $config + */ + public function test($config) + { + $this->taxConfig->open(); + $this->taxConfig->getTaxConfigForm()->fill($config); + $this->taxConfig->getPageActions()->save(); + } + + /** + * Restore previous tax configuration. + */ + protected function tearDown() + { + $this->taxConfig->open(); + $this->taxConfig->getTaxConfigForm()->rollback(); + $this->taxConfig->getPageActions()->save(); + } +} \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml new file mode 100644 index 0000000000000..73a33ec822902 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml @@ -0,0 +1,18 @@ + + + + + + Excluding Tax + After Discount + Including Tax + + + + + \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/etc/di.xml index e1900ce8d7ce6..9d0aa255b30b9 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/etc/di.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/etc/di.xml @@ -81,4 +81,14 @@ middle + + + high + + + + + high + + diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml new file mode 100644 index 0000000000000..ce68469ab8890 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file From 191e98e183b5d580189d08d0c3392b3c3963b599 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 17:38:29 +0300 Subject: [PATCH 140/363] MAGETWO-61267: Change format date for update and add ability to run cron from functional test --- .../Unit/Component/Form/Element/DataType/DateTest.php | 8 ++++---- lib/web/moment-timezone-with-data.js | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php index 9d37735bec196..b577e058be860 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/DateTest.php @@ -13,7 +13,7 @@ use Magento\Framework\View\Element\UiComponent\Processor; /** - * Tests Magento\Ui\Test\Unit\Component\Form\Element\DataType Class + * Tests Magento\Ui\Test\Unit\Component\Form\Element\DataType Class. */ class DateTest extends \PHPUnit_Framework_TestCase { @@ -46,7 +46,7 @@ public function setUp() } /** - * This tests ensures that outputDateFormat is properly saved in the configuration with timeOffset. + * This test ensures that outputDateFormat is properly saved in the configuration with timeOffset. */ public function testPrepareWithTimeOffset() { @@ -82,7 +82,7 @@ public function testPrepareWithTimeOffset() } /** - * This tests ensures that outputDateFormat is properly saved in the configuration without timeOffset. + * This test ensures that outputDateFormat is properly saved in the configuration without timeOffset. */ public function testPrepareWithoutTimeOffset() { @@ -126,7 +126,7 @@ public function testPrepareWithoutTimeOffset() } /** - * This tests ensures that userTimeZone is properly saved in the configuration. + * This test ensures that userTimeZone is properly saved in the configuration. */ public function testPrepare() { diff --git a/lib/web/moment-timezone-with-data.js b/lib/web/moment-timezone-with-data.js index 59c9b7540ec49..6dfd9559f93d4 100644 --- a/lib/web/moment-timezone-with-data.js +++ b/lib/web/moment-timezone-with-data.js @@ -1,7 +1,3 @@ -/** - * Copyright © 2013-2017 Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ //! moment-timezone.js //! version : 0.5.5 //! author : Tim Wood From dea0a1bd57a1e8cfa5440b2d8b8c57e761a7b75f Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Mon, 29 May 2017 17:47:40 +0300 Subject: [PATCH 141/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../TestSuite/InjectableTests/MAGETWO-61131.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml deleted file mode 100644 index ce68469ab8890..0000000000000 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-61131.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file From 59725db7a22710dcaea7d6438b51e12475a1f914 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Mon, 29 May 2017 18:47:29 +0300 Subject: [PATCH 142/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php index 9b21387d4dc04..836fa73f8a010 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php @@ -163,4 +163,4 @@ public function testMatchCmsControllerRouterMatchBeforeEventParams() $this->assertEquals($actionMock, $this->router->match($requestMock)); } -} \ No newline at end of file +} From 81cc5f9144a6c2caa57e30694c15e0b47b20dccf Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 30 May 2017 09:42:37 +0300 Subject: [PATCH 143/363] MAGETWO-59775: static content deployment does not generate secure content --- .../Mtf/Util/Command/Cli/StaticContent.php | 30 ++++ .../CurlTransport/BackendDecorator.php | 2 + .../Constraint/AssertHttpUsedOnFrontend.php | 91 ++++++++++ .../Constraint/AssertHttpsUsedOnBackend.php | 101 +++++++++++ .../Backend/Test/Repository/ConfigData.xml | 19 ++- .../Test/TestCase/ConfigureSecureUrlsTest.php | 159 ++++++++++++++++++ .../Test/TestCase/ConfigureSecureUrlsTest.xml | 20 +++ .../Customer/Test/Handler/Customer/Webapi.php | 1 + dev/tests/functional/utils/command.php | 1 + .../View/Asset/File/FallbackContext.php | 6 +- .../Framework/View/Asset/Repository.php | 1 - .../Unit/Asset/File/FallbackContextTest.php | 9 +- .../View/Test/Unit/Asset/RepositoryTest.php | 50 +++--- 13 files changed, 454 insertions(+), 36 deletions(-) create mode 100644 dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/StaticContent.php create mode 100644 dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php create mode 100644 dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php create mode 100644 dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.xml diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/StaticContent.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/StaticContent.php new file mode 100644 index 0000000000000..225b99b0283f6 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/StaticContent.php @@ -0,0 +1,30 @@ +transport->addOption(CURLOPT_SSL_VERIFYPEER, false); $this->transport->write($url, [], CurlInterface::GET); $this->read(); diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php new file mode 100644 index 0000000000000..e7937da1e4ec8 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php @@ -0,0 +1,91 @@ +browser = $browser; + $this->customer = $customer; + $this->customer->persist(); + + // Log in to Customer Account on Storefront to assert that http is used indeed. + $this->objectManager->create(LogInCustomerOnStorefront::class, ['customer' => $this->customer])->run(); + $this->assertUsedProtocol($this->unsecuredProtocol); + + // Log out from Customer Account on Storefront to assert that JS is deployed validly as a part of statics. + $this->objectManager->create(LogOutCustomerOnStorefront::class)->run(); + $this->assertUsedProtocol($this->unsecuredProtocol); + } + + /** + * Assert that specified protocol is used on current page. + * + * @param string $expectedProtocol + * @return void + */ + protected function assertUsedProtocol($expectedProtocol) + { + if (substr($expectedProtocol, -3) !== "://") { + $expectedProtocol .= '://'; + } + + \PHPUnit_Framework_Assert::assertStringStartsWith( + $expectedProtocol, + $this->browser->getUrl(), + "$expectedProtocol is not used." + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Unsecured URLs are used for Storefront pages.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php new file mode 100644 index 0000000000000..34019f5849be6 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php @@ -0,0 +1,101 @@ +browser = $browser; + + // Open specified Admin page using Navigation Menu to assert that JS is deployed validly as a part of statics. + $adminDashboardPage->open()->getMenuBlock()->navigate($navMenuPath); + $this->assertUsedProtocol($this->securedProtocol); + $this->assertDirectHttpUnavailable(); + } + + /** + * Assert that specified protocol is used on current page. + * + * @param string $expectedProtocol + * @return void + */ + protected function assertUsedProtocol($expectedProtocol) + { + if (substr($expectedProtocol, -3) !== "://") { + $expectedProtocol .= '://'; + } + + \PHPUnit_Framework_Assert::assertStringStartsWith( + $expectedProtocol, + $this->browser->getUrl(), + "$expectedProtocol is not used." + ); + } + + /** + * Assert that Merchant is redirected to https if trying to access the page directly via http. + * + * @return void + */ + protected function assertDirectHttpUnavailable() + { + $fakeUrl = str_replace($this->securedProtocol, $this->unsecuredProtocol, $this->browser->getUrl()); + $this->browser->open($fakeUrl); + \PHPUnit_Framework_Assert::assertStringStartsWith( + $this->securedProtocol, + $this->browser->getUrl(), + 'Merchant is not redirected to https if tries to access the Admin panel page directly via http.' + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Secured URLs are used for Admin panel pages.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml index 5180de367eb26..9ebc1c1b799ad 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml @@ -5,7 +5,8 @@ * See COPYING.txt for license details. */ --> - + @@ -156,12 +157,14 @@ 0 Yes 1 + 1 default 0 Yes 1 + 1 @@ -194,5 +197,19 @@ 0
    + + + default + 0 + No + 0 + + + default + 0 + No + 0 + + diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php new file mode 100644 index 0000000000000..7d515ab0a7f08 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php @@ -0,0 +1,159 @@ + Configuration" page. + * 3. Select needed scope. + * 4. Go to "General > Web > Base URLs (Secure)" section. + * 5. Specify Base URL with Secure protocol in the same format as a Secure Base URL. + * (i) Make sure that Secure Base URL ends with a "/". + * 6. Enable Secure URLs for Storefront if there is a need. + * 7. Enable Secure URLs for Admin if there is a need. + * 8. Save the Config & refresh invalidated caches (Configuration, Page Cache). + * 9. Deploy static view files. + * + * 10. If Secure URLs for Storefront were enabled: + * 1. Assert that https is used all over the Storefront. + * 2. Assert that static content is deployed validly (ex: JS functionality works on Storefront). + * 3. Assert that Customer is redirected to https if trying to access the page directly via http. + * 11. If secure URLs for Storefront were disabled: + * 1. Assert that http is used all over the Storefront. + * 2. Assert that static content is deployed validly (ex: JS functionality works on Storefront). + * + * 12. If secure URLs for Admin were enabled: + * 1. Assert that https is used all over the Admin panel. + * 2. Assert that static content is deployed validly (ex: JS functionality works in Admin panel). + * 3. Assert that Merchant is redirected to https if trying to access the page directly via http. + * 13. If secure URLs for Admin were disabled: + * 1. Assert that http is used all over the Admin panel. + * 2. Assert that static content is deployed validly (ex: JS functionality works in Admin panel). + * 3. Assert that Merchant is redirected to http if trying to access the page directly via https. + * + * Postconditions: + * 1. Turn the Secure URLs usage off (with further cache refreshing & static content deploying). + * + * @ZephyrId MAGETWO-63760 + */ +class ConfigureSecureUrlsTest extends Injectable +{ + /* tags */ + const MVP = 'no'; + const SEVERITY = 'S1'; + /* end tags */ + + /** + * Fixture factory. + * + * @var FixtureFactory + */ + protected $fixtureFactory; + + /** + * "Configuration" page in Admin panel. + * + * @var SystemConfigEdit + */ + protected $configurationAdminPage; + + /** + * Cache CLI. + * + * @var Cache + */ + protected $cache; + + /** + * Static content CLI. + * + * @var StaticContent + */ + protected $staticContent; + + /** + * Prepare data for further test execution. + * + * @param FixtureFactory $fixtureFactory + * @param SystemConfigEdit $configurationAdminPage + * @param Cache $cache + * @param StaticContent $staticContent + * @return void + */ + public function __inject( + FixtureFactory $fixtureFactory, + SystemConfigEdit $configurationAdminPage, + Cache $cache, + StaticContent $staticContent + ) { + $this->fixtureFactory = $fixtureFactory; + $this->configurationAdminPage = $configurationAdminPage; + $this->cache = $cache; + $this->staticContent = $staticContent; + } + + /** + * Test execution. + * + * @param string $configData + * @return void + */ + public function test($configData) + { + $data = [ + 'web/secure/base_url' => [ + 'scope' => 'default', + 'scope_id' => 0, + 'value' => str_replace(['http', 'index.php/'], ['https', ''], $_ENV['app_frontend_url']) + ] + ]; + $config = $this->fixtureFactory->createByCode('configData', ['dataset' => $configData, 'data' => $data]); + $config->persist(); + + // Workaround until MTA-3879 is delivered. + $this->configurationAdminPage->open(); + $this->configurationAdminPage->getForm() + ->getGroup('web', 'secure') + ->setValue('web', 'secure', 'use_in_adminhtml', 'Yes'); + $this->configurationAdminPage->getPageActions()->save(); + $_ENV['app_backend_url'] = str_replace('http', 'https', $_ENV['app_backend_url']); + + $this->cache->flush(['config', 'full_page']); + $this->staticContent->deploy(); + } + + /** + * Revert all applied high-level changes. + * + * @return void + */ + public function tearDown() + { + $configAdminPage = \Magento\Mtf\ObjectManagerFactory::getObjectManager()->create(SystemConfigEdit::class); + $configAdminPage->open(); + $configAdminPage->getForm() + ->getGroup('web', 'secure') + ->setValue('web', 'secure', 'use_in_adminhtml', 'No'); + $configAdminPage->getPageActions()->save(); + + $this->cache->flush(['config', 'full_page']); + $this->staticContent->deploy(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.xml new file mode 100644 index 0000000000000..3e5251167235d --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.xml @@ -0,0 +1,20 @@ + + + + + + disable_https_frontend_admin + Marketing>Catalog Price Rule + + + + + diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Handler/Customer/Webapi.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Handler/Customer/Webapi.php index cdd2c056d92bb..a32000ad466ba 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Handler/Customer/Webapi.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Handler/Customer/Webapi.php @@ -55,6 +55,7 @@ public function persist(FixtureInterface $customer = null) $data = $this->prepareData($customer); $url = $_ENV['app_frontend_url'] . 'rest/V1/customers'; + $this->webapiTransport->addOption(CURLOPT_SSL_VERIFYPEER, false); $this->webapiTransport->write($url, $data); $response = json_decode($this->webapiTransport->read(), true); $this->webapiTransport->close(); diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index d1bd30d5538d7..60966b7a5412a 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -8,6 +8,7 @@ 'cache:flush', 'cache:disable', 'cache:enable', + 'setup:static-content:deploy' ]; if (isset($_GET['command'])) { diff --git a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php index 424794c4f3064..4fb48b91fe0f6 100644 --- a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php +++ b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php @@ -15,6 +15,8 @@ class FallbackContext extends Context { /** * Secure path + * + * @deprecated */ const SECURE_PATH = 'secure'; @@ -35,6 +37,8 @@ class FallbackContext extends Context /** * @var bool + * + * @deprecated */ private $isSecure; @@ -103,6 +107,6 @@ private function generatePath() */ public function getConfigPath() { - return $this->getPath() . ($this->isSecure ? '/' . self::SECURE_PATH : ''); + return $this->getPath(); } } diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 2ef6678aaf739..37f737c6a6101 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -270,7 +270,6 @@ private function getFallbackContext($urlType, $isSecure, $area, $themePath, $loc 'areaType' => $area, 'themePath' => $themePath, 'localeCode' => $locale, - 'isSecure' => $isSecure ] ); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/File/FallbackContextTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/File/FallbackContextTest.php index 41e3d147067e8..625bf3708fbb3 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/File/FallbackContextTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/File/FallbackContextTest.php @@ -31,7 +31,6 @@ protected function setUp() * @param string $areaType * @param string $themePath * @param string $localeCode - * @param bool $isSecure * @param string $expectedResult * @dataProvider getConfigPathDataProvider */ @@ -40,17 +39,15 @@ public function testGetConfigPath( $areaType, $themePath, $localeCode, - $isSecure, $expectedResult ) { $this->fallbackContext = $this->objectManager->getObject( - 'Magento\Framework\View\Asset\File\FallbackContext', + \Magento\Framework\View\Asset\File\FallbackContext::class, [ 'baseUrl' => $baseUrl, 'areaType' => $areaType, 'themePath' => $themePath, 'localeCode' => $localeCode, - 'isSecure' => $isSecure ] ); $this->assertEquals($expectedResult, $this->fallbackContext->getConfigPath()); @@ -64,7 +61,6 @@ public function getConfigPathDataProvider() 'areaType' => 'frontend', 'themePath' => 'Magento/blank', 'localeCode' => 'en_US', - 'isSecure' => false, 'expectedResult' => 'frontend/Magento/blank/en_US' ], 'https' => [ @@ -72,8 +68,7 @@ public function getConfigPathDataProvider() 'areaType' => 'frontend', 'themePath' => 'Magento/blank', 'localeCode' => 'en_US', - 'isSecure' => true, - 'expectedResult' => 'frontend/Magento/blank/en_US/secure' + 'expectedResult' => 'frontend/Magento/blank/en_US' ] ]; } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php index 2a0e52434cc56..be0e0a85f14d1 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php @@ -11,7 +11,9 @@ use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** - * Unit test for Magento\Framework\View\Asset\Repository + * Unit test for Magento\Framework\View\Asset\Repository. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RepositoryTest extends \PHPUnit_Framework_TestCase { @@ -70,33 +72,35 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->urlMock = $this->getMockBuilder('Magento\Framework\UrlInterface') + $this->urlMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->designMock = $this->getMockBuilder('Magento\Framework\View\DesignInterface') + $this->designMock = $this->getMockBuilder(\Magento\Framework\View\DesignInterface::class) ->disableOriginalConstructor() ->getMock(); $this->themeProvider = $this->getMockBuilder(ThemeProviderInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->sourceMock = $this->getMockBuilder('Magento\Framework\View\Asset\Source') + $this->sourceMock = $this->getMockBuilder(\Magento\Framework\View\Asset\Source::class) ->disableOriginalConstructor() ->getMock(); - $this->httpMock = $this->getMockBuilder('Magento\Framework\App\Request\Http') + $this->httpMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() ->getMock(); - $this->fileFactoryMock = $this->getMockBuilder('Magento\Framework\View\Asset\FileFactory') + $this->fileFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Asset\FileFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->fallbackFactoryMock = $this->getMockBuilder('Magento\Framework\View\Asset\File\FallbackContextFactory') + $this->fallbackFactoryMock = $this->getMockBuilder( + \Magento\Framework\View\Asset\File\FallbackContextFactory::class + ) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->contextFactoryMock = $this->getMockBuilder('Magento\Framework\View\Asset\File\ContextFactory') + $this->contextFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File\ContextFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->remoteFactoryMock = $this->getMockBuilder('Magento\Framework\View\Asset\RemoteFactory') + $this->remoteFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Asset\RemoteFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); @@ -172,7 +176,7 @@ public function testCreateAsset() ->method('getThemeByFullPath') ->willReturnArgument(0); - $fallbackContextMock = $this->getMockBuilder('Magento\Framework\View\Asset\File\FallbackContex') + $fallbackContextMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File\FallbackContex::class) ->disableOriginalConstructor() ->getMock(); $this->fallbackFactoryMock @@ -184,12 +188,11 @@ public function testCreateAsset() 'areaType' => '', 'themePath' => 'Default', 'localeCode' => '', - 'isSecure' => '', ] ) ->willReturn($fallbackContextMock); - $assetMock = $this->getMockBuilder('Magento\Framework\View\Asset\File') + $assetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) ->disableOriginalConstructor() ->getMock(); @@ -218,7 +221,7 @@ public function testCreateAsset() */ public function testGetStaticViewFileContext() { - $themeMock = $this->getMock('Magento\Framework\View\Design\ThemeInterface', [], [], '', false); + $themeMock = $this->getMock(\Magento\Framework\View\Design\ThemeInterface::class, [], [], '', false); $this->designMock ->expects($this->any()) ->method('getDesignParams') @@ -233,12 +236,8 @@ public function testGetStaticViewFileContext() ->expects($this->any()) ->method('getThemeByFullPath') ->willReturnArgument(0); - $this->httpMock - ->expects($this->any()) - ->method('isSecure') - ->willReturn(false); - $fallbackContextMock = $this->getMockBuilder('Magento\Framework\View\Asset\File\FallbackContex') + $fallbackContextMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File\FallbackContex::class) ->disableOriginalConstructor() ->getMock(); $this->fallbackFactoryMock @@ -250,7 +249,6 @@ public function testGetStaticViewFileContext() 'areaType' => 'area', 'themePath' => '', 'localeCode' => 'locale', - 'isSecure' => '', ] ) ->willReturn($fallbackContextMock); @@ -270,11 +268,11 @@ public function testGetStaticViewFileContext() */ public function testCreateRelated($filePath, $resultFilePath, $module) { - $originalContextMock = $this->getMockBuilder('Magento\Framework\View\Asset\ContextInterface') + $originalContextMock = $this->getMockBuilder(\Magento\Framework\View\Asset\ContextInterface::class) ->disableOriginalConstructor() ->getMock(); - $originalAssetMock = $this->getMockBuilder('Magento\Framework\View\Asset\File') + $originalAssetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) ->disableOriginalConstructor() ->setMethods(['getModule', 'getContext']) ->getMock(); @@ -283,7 +281,7 @@ public function testCreateRelated($filePath, $resultFilePath, $module) ->method('getContext') ->willReturn($originalContextMock); - $assetMock = $this->getMockBuilder('Magento\Framework\View\Asset\File') + $assetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) ->disableOriginalConstructor() ->getMock(); @@ -323,7 +321,7 @@ public function createRelatedDataProvider() */ public function testCreateArbitrary() { - $contextMock = $this->getMockBuilder('Magento\Framework\View\Asset\ContextInterface') + $contextMock = $this->getMockBuilder(\Magento\Framework\View\Asset\ContextInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -339,7 +337,7 @@ public function testCreateArbitrary() ) ->willReturn($contextMock); - $assetMock = $this->getMockBuilder('Magento\Framework\View\Asset\File') + $assetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) ->disableOriginalConstructor() ->getMock(); @@ -375,7 +373,7 @@ public function testCreateRemoteAsset() */ public function testGetUrl() { - $themeMock = $this->getMock('Magento\Framework\View\Design\ThemeInterface', [], [], '', false); + $themeMock = $this->getMock(\Magento\Framework\View\Design\ThemeInterface::class, [], [], '', false); $this->designMock ->expects($this->any()) ->method('getDesignParams') @@ -387,7 +385,7 @@ public function testGetUrl() ] ); - $assetMock = $this->getMockBuilder('Magento\Framework\View\Asset\File') + $assetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) ->disableOriginalConstructor() ->getMock(); $assetMock From a19e65fb02ea6db577dd6395c49c9c596b877820 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Tue, 30 May 2017 10:42:32 +0300 Subject: [PATCH 144/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Theme/Block/Html/Topmenu.php | 8 +++++++- .../Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index d888a037414ba..52fd3d5d346a7 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -103,6 +103,7 @@ public function getHtml($outermostClass = '', $childrenWrapClass = '', $limit = ['menu' => $this->getMenu(), 'transportObject' => $transportObject] ); $html = $transportObject->getHtml(); + return $html; } @@ -121,6 +122,7 @@ protected function _countItems($items) $total += $this->_countItems($item->getChildren()); } } + return $total; } @@ -277,6 +279,7 @@ protected function _getRenderedMenuItemAttributes(\Magento\Framework\Data\Tree\N foreach ($attributes as $attributeName => $attributeValue) { $html .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"'; } + return $html; } @@ -289,6 +292,7 @@ protected function _getRenderedMenuItemAttributes(\Magento\Framework\Data\Tree\N protected function _getMenuItemAttributes(\Magento\Framework\Data\Tree\Node $item) { $menuItemClasses = $this->_getMenuItemClasses($item); + return ['class' => implode(' ', $menuItemClasses)]; } @@ -362,6 +366,7 @@ public function getCacheKeyInfo() { $keyInfo = parent::getCacheKeyInfo(); $keyInfo[] = $this->getUrl('*/*/*', ['_current' => true, '_query' => '']); + return $keyInfo; } @@ -394,6 +399,7 @@ public function getMenu() ] ); } + return $this->_menu; } -} \ No newline at end of file +} diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index c0d68efbbd626..a564b72f40886 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -312,4 +312,4 @@ public function testGetMenu() $topmenuBlock = $this->getTopmenu(); $this->assertEquals($nodeMock, $topmenuBlock->getMenu()); } -} \ No newline at end of file +} From 6314205f7af6de4ccddd1469433a48cc56f74ccf Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Tue, 30 May 2017 11:01:46 +0300 Subject: [PATCH 145/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Theme/Block/Html/Topmenu.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 52fd3d5d346a7..7cc741cd4dfbe 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -247,9 +247,9 @@ protected function _getHtml( } $html .= '
  • _getRenderedMenuItemAttributes($child) . '>'; - $html .= '' . $this->escapeHtml( - $child->getName() - ) . '' . $this->_addSubMenu( + $html .= '' + . $this->escapeHtml($child->getName()) + . '' . $this->_addSubMenu( $child, $childLevel, $childrenWrapClass, From 97b10fa67dc239daf3d941b5a3069d984a409dde Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 30 May 2017 11:21:40 +0300 Subject: [PATCH 146/363] MAGETWO-57607: [Backport] - Unable to save product with all unchecked values for multiple select attribute - for 2.1 #7687 --- lib/web/mage/utils/misc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index c8f7a0481f600..c628ad229e9bd 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -221,9 +221,10 @@ define([ data = data || {}; suffix = suffix || 'prepared-for-send'; separator = separator || '-'; + _.each(data, function (value, key) { if (_.isObject(value) && !value.length) { - this.filterFormData(value, suffix, separator) + this.filterFormData(value, suffix, separator); } else if (_.isString(key) && ~key.indexOf(suffix)) { data[key.split(separator)[0]] = value; delete data[key]; From 0604157b7bd274d448dc90606d89e808bed86af2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 30 May 2017 12:01:35 +0300 Subject: [PATCH 147/363] MAGETWO-57475: [Backport] - Layered navigation contains filters for out of stock products - for 2.1 --- .../Magento/Framework/Search/Adapter/Mysql/AdapterTest.php | 6 +++--- .../testsuite/Magento/Framework/Search/_files/requests.xml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 0ea897c8dcf76..a5b24354ff831 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 @@ -424,7 +424,7 @@ public function testAdvancedSearchConfigProductWithOutOfStockOption() { /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ $attribute = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable'); + ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable_searchable'); /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $selectOptions */ $selectOptions = $this->objectManager ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class) @@ -432,7 +432,7 @@ public function testAdvancedSearchConfigProductWithOutOfStockOption() $firstOption = $selectOptions->getFirstItem(); $firstOptionId = $firstOption->getId(); - $this->requestBuilder->bind('test_configurable', $firstOptionId); + $this->requestBuilder->bind('test_configurable_searchable', $firstOptionId); $this->requestBuilder->setRequestName('filter_out_of_stock_child'); $queryResponse = $this->executeQuery(); @@ -441,7 +441,7 @@ public function testAdvancedSearchConfigProductWithOutOfStockOption() $secondOption = $selectOptions->getLastItem(); $secondOptionId = $secondOption->getId(); - $this->requestBuilder->bind('test_configurable', $secondOptionId); + $this->requestBuilder->bind('test_configurable_searchable', $secondOptionId); $this->requestBuilder->setRequestName('filter_out_of_stock_child'); $queryResponse = $this->executeQuery(); 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 835189f56f4b4..25d667d0d547c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml @@ -393,14 +393,14 @@ - + - + - + 0 From 5cc618530f455524c9ed7bdf2cefe96f7ad53b8a Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 30 May 2017 12:58:55 +0300 Subject: [PATCH 148/363] MAGETWO-57475: [Backport] - Layered navigation contains filters for out of stock products - for 2.1 --- .../Test/Unit/Model/Search/FilterMapper/FilterContextTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php index 119855a131475..2522a14f1be79 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php @@ -18,6 +18,8 @@ /** * Tests FilterContext class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FilterContextTest extends \PHPUnit_Framework_TestCase { From 15cfb29375f13f65ef442f73e439f65b78b35a5d Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 14:12:12 +0300 Subject: [PATCH 149/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../Notification/ApplyDiscountOnPrices.php | 16 ++++++++--- .../Message/Notification/DiscountErrors.php | 18 ++++++++---- .../Message/Notification/RoundingErrors.php | 10 ++++++- .../Model/System/Message/Notifications.php | 28 +++++++++++-------- .../ApplyDiscountOnPricesTest.php | 6 ++-- .../Notification/DiscountErrorsTest.php | 6 ++-- .../Notification/RoundingErrorsTest.php | 6 ++-- .../System/Message/NotificationsTest.php | 8 +++--- 8 files changed, 63 insertions(+), 35 deletions(-) diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php index 4e5497dbdff2b..b8e808ad514e3 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php @@ -17,16 +17,22 @@ class ApplyDiscountOnPrices implements \Magento\Tax\Model\System\Message\NotificationInterface { /** + * Store manager object. + * * @var \Magento\Store\Model\StoreManagerInterface */ private $storeManager; /** + * Store url interface object. + * * @var \Magento\Framework\UrlInterface */ private $urlBuilder; /** + * Store tax configuration. + * * @var Config */ private $taxConfig; @@ -70,6 +76,7 @@ public function isDisplayed() if (!$this->taxConfig->isWrongApplyDiscountSettingIgnored() && $this->getStoresWithWrongSettings()) { return true; } + return false; } @@ -125,19 +132,20 @@ private function getStoresWithWrongSettings() $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; } } + return $this->storesWithInvalidSettings; } /** * Check if settings are valid. * - * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @param null|int|bool|string|\Magento\Store\Model\Store $store * @return bool false if settings are incorrect */ private function checkSettings($store = null) { return $this->taxConfig->priceIncludesTax($store) - || !$this->taxConfig->applyTaxAfterDiscount($store) - || !$this->taxConfig->discountTax($store); + || !$this->taxConfig->applyTaxAfterDiscount($store) + || !$this->taxConfig->discountTax($store); } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php index dd36a40d1c5a2..7c34a3aba678f 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php @@ -13,30 +13,34 @@ class DiscountErrors implements \Magento\Tax\Model\System\Message\NotificationInterface { /** + * Store manager interface. + * * @var \Magento\Store\Model\StoreManagerInterface */ private $storeManager; /** + * Store Url Interface object. + * * @var \Magento\Framework\UrlInterface */ private $urlBuilder; /** + * Store tax configuration object. + * * @var \Magento\Tax\Model\Config */ private $taxConfig; /** - * Websites with invalid discount settings + * Websites with invalid discount settings. * * @var array */ private $storesWithInvalidSettings; /** - * Initialize dependencies - * * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\UrlInterface $urlBuilder * @param \Magento\Tax\Model\Config $taxConfig @@ -68,6 +72,7 @@ public function isDisplayed() if (!$this->taxConfig->isWrongDiscountSettingsIgnored() && $this->getStoresWithWrongSettings()) { return true; } + return false; } @@ -106,13 +111,13 @@ public function getSeverity() } /** - * Check if tax discount settings are compatible + * Check if tax discount settings are compatible. * * Matrix for invalid discount settings is as follows: * Before Discount / Excluding Tax * Before Discount / Including Tax * - * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @param null|int|bool|string|\Magento\Store\Model\Store $store * @return bool */ private function checkSettings($store = null) @@ -139,6 +144,7 @@ private function getStoresWithWrongSettings() $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; } } + return $this->storesWithInvalidSettings; } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php index f32cde63eb08e..bf9be66a2783d 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php @@ -13,16 +13,22 @@ class RoundingErrors implements \Magento\Tax\Model\System\Message\NotificationInterface { /** + * Store manager object. + * * @var \Magento\Store\Model\StoreManagerInterface */ private $storeManager; /** + * Store Url builder object. + * * @var \Magento\Framework\UrlInterface */ private $urlBuilder; /** + * Store Tax config object. + * * @var \Magento\Tax\Model\Config */ private $taxConfig; @@ -66,6 +72,7 @@ public function isDisplayed() if (!$this->taxConfig->isWrongDisplaySettingsIgnored() && $this->getStoresWithWrongSettings()) { return true; } + return false; } @@ -109,7 +116,7 @@ public function getSeverity() * Tax Calculation Method Based On 'Total' or 'Row' * and at least one Price Display Settings has 'Including and Excluding Tax' value. * - * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @param null|int|bool|string|\Magento\Store\Model\Store $store * @return bool */ private function checkSettings($store = null) @@ -146,6 +153,7 @@ private function getStoresWithWrongSettings() $this->storesWithInvalidSettings[] = $website->getName() . ' (' . $store->getName() . ')'; } } + return $this->storesWithInvalidSettings; } } \ No newline at end of file diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index dacd9770a49aa..51c5146a3b669 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -11,7 +11,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface { /** - * Store manager object + * Store manager object. * * @var \Magento\Store\Model\StoreManagerInterface * @deprecated @@ -19,19 +19,21 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface protected $storeManager; /** + * Store url interface object. + * * @var \Magento\Framework\UrlInterface */ protected $urlBuilder; /** - * Tax configuration object + * Tax configuration object. * * @var \Magento\Tax\Model\Config */ protected $taxConfig; /** - * Stores with invalid display settings + * Stores with invalid display settings. * * @var array * @deprecated @@ -40,7 +42,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface protected $storesWithInvalidDisplaySettings; /** - * Websites with invalid discount settings + * Websites with invalid discount settings. * * @var array * @deprecated @@ -49,6 +51,8 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface protected $storesWithInvalidDiscountSettings; /** + * Array with notification objects. + * * @var NotificationInterface[] */ private $notifications = []; @@ -119,7 +123,7 @@ public function getText() } /** - * Retrieve message severity + * Retrieve message severity. * * @return int * @codeCoverageIgnore @@ -130,7 +134,7 @@ public function getSeverity() } /** - * Get URL for the tax notification documentation + * Get URL for the tax notification documentation. * * @return string */ @@ -140,7 +144,7 @@ public function getInfoUrl() } /** - * Get URL to the admin tax configuration page + * Get URL to the admin tax configuration page. * * @return string */ @@ -150,13 +154,13 @@ public function getManageUrl() } /** - * Check if tax calculation type and price display settings are compatible + * Check if tax calculation type and price display settings are compatible. * * Invalid settings if * Tax Calculation Method Based On 'Total' or 'Row' * and at least one Price Display Settings has 'Including and Excluding Tax' value * - * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @param null|int|bool|string|\Magento\Store\Model\Store $store * @return bool * @deprecated * @see \Magento\Tax\Model\System\Message\Notification\RoundingErrors::checkSettings @@ -166,6 +170,7 @@ public function checkDisplaySettings($store = null) if ($this->taxConfig->getAlgorithm($store) == \Magento\Tax\Model\Calculation::CALC_UNIT_BASE) { return true; } + return $this->taxConfig->getPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH && !$this->taxConfig->displayCartPricesBoth($store) @@ -177,7 +182,7 @@ public function checkDisplaySettings($store = null) } /** - * Check if tax discount settings are compatible + * Check if tax discount settings are compatible. * * Matrix for invalid discount settings is as follows: * Before Discount / Excluding Tax @@ -194,7 +199,7 @@ public function checkDiscountSettings($store = null) } /** - * Get URL to ignore tax notifications + * Get URL to ignore tax notifications. * * @param string $section * @return string @@ -223,6 +228,7 @@ public function getStoresWithWrongDisplaySettings() $storeNames[] = $website->getName() . '(' . $store->getName() . ')'; } } + return $storeNames; } diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php index 8baf6e421a3d5..37fef37e0d3f5 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php @@ -29,17 +29,17 @@ class ApplyDiscountOnPricesTest extends \PHPUnit_Framework_TestCase private $applyDiscountOnPricesNotification; /** - * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var UrlInterface | \PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ private $urlBuilderMock; /** - * @var TaxConfig | \PHPUnit_Framework_MockObject_MockObject + * @var TaxConfig|\PHPUnit_Framework_MockObject_MockObject */ private $taxConfigMock; diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php index 1901cd543ce53..1a807bf458709 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php @@ -24,17 +24,17 @@ class DiscountErrorsTest extends \PHPUnit_Framework_TestCase private $discountErrorsNotification; /** - * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var UrlInterface | \PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ private $urlBuilderMock; /** - * @var TaxConfig | \PHPUnit_Framework_MockObject_MockObject + * @var TaxConfig|\PHPUnit_Framework_MockObject_MockObject */ private $taxConfigMock; diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php index 8568f786aba4c..ec799937614f4 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php @@ -24,17 +24,17 @@ class RoundingErrorsTest extends \PHPUnit_Framework_TestCase private $roundingErrorsNotification; /** - * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var UrlInterface | \PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ private $urlBuilderMock; /** - * @var TaxConfig | \PHPUnit_Framework_MockObject_MockObject + * @var TaxConfig|\PHPUnit_Framework_MockObject_MockObject */ private $taxConfigMock; diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php index 522bd68f90cbb..5233f26408546 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php @@ -24,22 +24,22 @@ class NotificationsTest extends \PHPUnit_Framework_TestCase private $notifications; /** - * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var UrlInterface | \PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ private $urlBuilderMock; /** - * @var TaxConfig | \PHPUnit_Framework_MockObject_MockObject + * @var TaxConfig|\PHPUnit_Framework_MockObject_MockObject */ private $taxConfigMock; /** - * @var NotificationInterface | \PHPUnit_Framework_MockObject_MockObject + * @var NotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $notificationMock; From 29ccc6b5c97e7e9e632bd23e44a93cea150f7362 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 14:38:38 +0300 Subject: [PATCH 150/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../Message/Notification/RoundingErrors.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php index bf9be66a2783d..fbd1d379c3f32 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php @@ -124,14 +124,15 @@ private function checkSettings($store = null) if ($this->taxConfig->getAlgorithm($store) == \Magento\Tax\Model\Calculation::CALC_UNIT_BASE) { return true; } + return $this->taxConfig->getPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH - && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH - && !$this->taxConfig->displayCartPricesBoth($store) - && !$this->taxConfig->displayCartSubtotalBoth($store) - && !$this->taxConfig->displayCartShippingBoth($store) - && !$this->taxConfig->displaySalesPricesBoth($store) - && !$this->taxConfig->displaySalesSubtotalBoth($store) - && !$this->taxConfig->displaySalesShippingBoth($store); + && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH + && !$this->taxConfig->displayCartPricesBoth($store) + && !$this->taxConfig->displayCartSubtotalBoth($store) + && !$this->taxConfig->displayCartShippingBoth($store) + && !$this->taxConfig->displaySalesPricesBoth($store) + && !$this->taxConfig->displaySalesSubtotalBoth($store) + && !$this->taxConfig->displaySalesShippingBoth($store); } /** From 006587fe580b5d21d38bfe89f629b69594b18ea3 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 14:42:13 +0300 Subject: [PATCH 151/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../Tax/Model/System/Message/Notifications.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index 51c5146a3b669..8a69e9c33d51a 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -172,13 +172,13 @@ public function checkDisplaySettings($store = null) } return $this->taxConfig->getPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH - && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH - && !$this->taxConfig->displayCartPricesBoth($store) - && !$this->taxConfig->displayCartSubtotalBoth($store) - && !$this->taxConfig->displayCartShippingBoth($store) - && !$this->taxConfig->displaySalesPricesBoth($store) - && !$this->taxConfig->displaySalesSubtotalBoth($store) - && !$this->taxConfig->displaySalesShippingBoth($store); + && $this->taxConfig->getShippingPriceDisplayType($store) != \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH + && !$this->taxConfig->displayCartPricesBoth($store) + && !$this->taxConfig->displayCartSubtotalBoth($store) + && !$this->taxConfig->displayCartShippingBoth($store) + && !$this->taxConfig->displaySalesPricesBoth($store) + && !$this->taxConfig->displaySalesSubtotalBoth($store) + && !$this->taxConfig->displaySalesShippingBoth($store); } /** @@ -188,7 +188,7 @@ public function checkDisplaySettings($store = null) * Before Discount / Excluding Tax * Before Discount / Including Tax * - * @param null|int|bool|string|\Magento\Store\Model\Store $store $store + * @param null|int|bool|string|\Magento\Store\Model\Store $store * @return bool * @deprecated * @see \Magento\Tax\Model\System\Message\Notification\DiscountErrors::checkSettings @@ -250,6 +250,7 @@ public function getStoresWithWrongDiscountSettings() $storeNames[] = $website->getName() . '(' . $store->getName() . ')'; } } + return $storeNames; } } \ No newline at end of file From 47fdd88c7dbabf5305eedfb55f82e6f097016e16 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 30 May 2017 16:14:55 +0300 Subject: [PATCH 152/363] MAGETWO-59775: static content deployment does not generate secure content --- .../Backend/Test/Constraint/AssertHttpUsedOnFrontend.php | 6 +++--- .../Backend/Test/Constraint/AssertHttpsUsedOnBackend.php | 6 +++--- .../Backend/Test/TestCase/ConfigureSecureUrlsTest.php | 8 ++++---- dev/tests/functional/utils/command.php | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php index e7937da1e4ec8..92eab50951540 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpUsedOnFrontend.php @@ -29,14 +29,14 @@ class AssertHttpUsedOnFrontend extends AbstractConstraint * * @var BrowserInterface */ - protected $browser; + private $browser; /** * Customer account. * * @var Customer */ - protected $customer; + private $customer; /** * Validations execution. @@ -66,7 +66,7 @@ public function processAssert(BrowserInterface $browser, Customer $customer) * @param string $expectedProtocol * @return void */ - protected function assertUsedProtocol($expectedProtocol) + private function assertUsedProtocol($expectedProtocol) { if (substr($expectedProtocol, -3) !== "://") { $expectedProtocol .= '://'; diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php index 34019f5849be6..8f85a4dd4d405 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertHttpsUsedOnBackend.php @@ -34,7 +34,7 @@ class AssertHttpsUsedOnBackend extends AbstractConstraint * * @var BrowserInterface */ - protected $browser; + private $browser; /** * Validations execution. @@ -60,7 +60,7 @@ public function processAssert(BrowserInterface $browser, Dashboard $adminDashboa * @param string $expectedProtocol * @return void */ - protected function assertUsedProtocol($expectedProtocol) + private function assertUsedProtocol($expectedProtocol) { if (substr($expectedProtocol, -3) !== "://") { $expectedProtocol .= '://'; @@ -78,7 +78,7 @@ protected function assertUsedProtocol($expectedProtocol) * * @return void */ - protected function assertDirectHttpUnavailable() + private function assertDirectHttpUnavailable() { $fakeUrl = str_replace($this->securedProtocol, $this->unsecuredProtocol, $this->browser->getUrl()); $this->browser->open($fakeUrl); diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php index 7d515ab0a7f08..14e50e8f23e4b 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigureSecureUrlsTest.php @@ -65,28 +65,28 @@ class ConfigureSecureUrlsTest extends Injectable * * @var FixtureFactory */ - protected $fixtureFactory; + private $fixtureFactory; /** * "Configuration" page in Admin panel. * * @var SystemConfigEdit */ - protected $configurationAdminPage; + private $configurationAdminPage; /** * Cache CLI. * * @var Cache */ - protected $cache; + private $cache; /** * Static content CLI. * * @var StaticContent */ - protected $staticContent; + private $staticContent; /** * Prepare data for further test execution. diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index 60966b7a5412a..adcc1d3baeea6 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -8,7 +8,7 @@ 'cache:flush', 'cache:disable', 'cache:enable', - 'setup:static-content:deploy' + 'setup:static-content:deploy', ]; if (isset($_GET['command'])) { From ccd427338f640e0843cf966446e7e8cf9af0fadd Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 16:33:31 +0300 Subject: [PATCH 153/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- app/code/Magento/Tax/Model/Config.php | 10 ++++------ .../System/Message/Notification/DiscountErrors.php | 8 +++++--- .../Magento/Tax/Model/System/Message/Notifications.php | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php index d8d510a11239f..0273d111ef780 100644 --- a/app/code/Magento/Tax/Model/Config.php +++ b/app/code/Magento/Tax/Model/Config.php @@ -763,12 +763,11 @@ public function crossBorderTradeEnabled($store = null) ); } - /* + /** * Check if admin notification related to misconfiguration of "Apply Discount On Prices" should be ignored. - * - * Warning is displayed in case when "Catalog Prices" = "Excluding Tax" - * AND "Apply Discount On Prices" = "Including Tax" - * AND "Apply Customer Tax" = "After Discount" + * Warning is displayed in case when "Catalog Prices" = "Excluding Tax" + * AND "Apply Discount On Prices" = "Including Tax" + * AND "Apply Customer Tax" = "After Discount" * * @param null|string|Store $store * @return bool @@ -782,7 +781,6 @@ public function isWrongApplyDiscountSettingIgnored($store = null) ); } - /** * Check if do not show notification about wrong display settings * diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php index 7c34a3aba678f..fdd72d684f0c6 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/DiscountErrors.php @@ -85,9 +85,11 @@ public function getText() if (!empty($this->getStoresWithWrongSettings()) && !$this->taxConfig->isWrongDiscountSettingsIgnored()) { $messageDetails .= ''; - $messageDetails .= __('With customer tax applied “Before Discount”,' . - ' the final discount calculation may not match customers’ expectations.'); - $messageDetails .= '

    '; + $messageDetails .= __( + 'With customer tax applied “Before Discount”,' . + ' the final discount calculation may not match customers’ expectations.' + ); + $messageDetails .= '

    '; $messageDetails .= __('Store(s) affected: '); $messageDetails .= implode(', ', $this->getStoresWithWrongSettings()); $messageDetails .= '

    '; diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index 8a69e9c33d51a..dd5606bd3f9a8 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -253,4 +253,4 @@ public function getStoresWithWrongDiscountSettings() return $storeNames; } -} \ No newline at end of file +} From 4b7431d279ffde2c570a6bd0cd05f06056b23903 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Tue, 30 May 2017 17:09:00 +0300 Subject: [PATCH 154/363] MAGETWO-61262: Add request parameter to CMS event observer --- app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php | 4 +++- app/code/Magento/Theme/Block/Html/Topmenu.php | 6 +++++- .../Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php | 8 +++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php index 836fa73f8a010..b3707c72dd98e 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php @@ -80,7 +80,9 @@ protected function setUp() } /** - * This tests ensures that cms controller router match cms page before event params. + * This test ensures that cms controller router match cms page before event params. + * + * @return void */ public function testMatchCmsControllerRouterMatchBeforeEventParams() { diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 7cc741cd4dfbe..56164e58394e7 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -35,16 +35,20 @@ class Topmenu extends Template implements IdentityInterface * * @var Registry * - * @deprecated The property can be removed in a future release. + * @deprecated */ protected $registry; /** + * Data tree node. + * * @var NodeFactory */ private $nodeFactory; /** + * Data tree. + * * @var TreeFactory */ private $treeFactory; diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index a564b72f40886..c0f2aa9036065 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -13,7 +13,7 @@ use Magento\Framework\Data\Tree\NodeFactory; /** - * Tests Magento\Theme\Block\Html\Topmenu Class + * Tests Magento\Theme\Block\Html\Topmenu Class. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -204,7 +204,7 @@ public function testGetCacheKeyInfo() } /** - * Create Tree Node mock object + * Create Tree Node mock object. * * Helper method, that provides unified logic of creation of Tree Node mock objects. * @@ -282,7 +282,9 @@ private function buildTree($isCurrentItem) } /** - * This tests ensures that getMenu object is equal \Magento\Framework\Data\Tree\Node root node object. + * This test ensures that getMenu object is equal \Magento\Framework\Data\Tree\Node root node object. + * + * @return void */ public function testGetMenu() { From 2d4e25c00767f8c6f7a4bbbf9769bc5665934ac7 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 17:47:45 +0300 Subject: [PATCH 155/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../System/Message/Notification/ApplyDiscountOnPrices.php | 6 ++++-- .../Model/System/Message/Notification/RoundingErrors.php | 2 +- .../Message/Notification/ApplyDiscountOnPricesTest.php | 2 +- .../System/Message/Notification/DiscountErrorsTest.php | 2 +- .../System/Message/Notification/RoundingErrorsTest.php | 2 +- .../Test/Unit/Model/System/Message/NotificationsTest.php | 2 +- .../Tax/Test/Block/Adminhtml/System/Config/Notification.php | 2 +- .../Block/Adminhtml/System/Config/NotificationPopup.php | 2 +- .../Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php | 2 +- .../AssertTaxConfigurationNotificationMessage.php | 2 +- .../Constraint/AssertTaxConfigurationSuccessSaveMessage.php | 2 +- .../Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml | 2 +- .../app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php | 2 +- .../app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml | 2 +- 14 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php index b8e808ad514e3..6c13373c1734a 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/ApplyDiscountOnPrices.php @@ -89,8 +89,10 @@ public function getText() if ($this->isDisplayed()) { $messageDetails .= ''; - $messageDetails .= __('To apply the discount on prices including tax and apply the tax after discount,'. - ' set Catalog Prices to “Including Tax”. '); + $messageDetails .= __( + 'To apply the discount on prices including tax and apply the tax after discount,'. + ' set Catalog Prices to “Including Tax”. ' + ); $messageDetails .= '

    '; $messageDetails .= __('Store(s) affected: '); $messageDetails .= implode(', ', $this->getStoresWithWrongSettings()); diff --git a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php index fbd1d379c3f32..cce0ad9d77e96 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php +++ b/app/code/Magento/Tax/Model/System/Message/Notification/RoundingErrors.php @@ -157,4 +157,4 @@ private function getStoresWithWrongSettings() return $this->storesWithInvalidSettings; } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php index 37fef37e0d3f5..149c0c9a17b3a 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/ApplyDiscountOnPricesTest.php @@ -157,4 +157,4 @@ public function testGetText() $this->applyDiscountOnPricesNotification->getText() ); } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php index 1a807bf458709..033790618d455 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/DiscountErrorsTest.php @@ -100,4 +100,4 @@ public function testGetText() $this->discountErrorsNotification->getText() ); } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php index ec799937614f4..c8244d3c61c31 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/Notification/RoundingErrorsTest.php @@ -139,4 +139,4 @@ public function testGetText() $this->roundingErrorsNotification->getText() ); } -} \ No newline at end of file +} diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php index 5233f26408546..ab325615b561a 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php @@ -94,4 +94,4 @@ public function testGetText() $this->notifications->getText() ); } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php index 004b7cf816c68..034d2e8746f6c 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Notification.php @@ -31,4 +31,4 @@ public function openNotificationPopup() $messageLink->click(); } } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php index 0a99c272495e8..747c1d6c1f340 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/NotificationPopup.php @@ -33,4 +33,4 @@ public function getNotificationMessage() return $message; } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php index aa0bb30f3446f..4d52f824fb029 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Block/Adminhtml/System/Config/Tax.php @@ -71,4 +71,4 @@ public function rollback() $this->fill($this->origData); } } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php index 0ebe69da95c66..ce76499a8617c 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationNotificationMessage.php @@ -47,4 +47,4 @@ public function toString() { return 'Tax notification message is present.'; } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php index e1d9874f8417c..e627df991736b 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Constraint/AssertTaxConfigurationSuccessSaveMessage.php @@ -41,4 +41,4 @@ public function toString() { return 'Save tax configuration success message is present.'; } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml index 79d31b62e6a15..c0331f70a5903 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/Page/Adminhtml/TaxConfiguration.xml @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php index 8ac4a2d0e9390..e6b25102b1c6f 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.php @@ -58,4 +58,4 @@ protected function tearDown() $this->taxConfig->getTaxConfigForm()->rollback(); $this->taxConfig->getPageActions()->save(); } -} \ No newline at end of file +} diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml index 73a33ec822902..9078141177412 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxConfigurationTest.xml @@ -15,4 +15,4 @@ - \ No newline at end of file + From 5448e0c90ad5f5bd27cdb9864b36b9ae5a3ab0bc Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 30 May 2017 18:24:53 +0300 Subject: [PATCH 156/363] MAGETWO-61131: [Backport] - Tax and Order total not correct when discount is applied - for 2.1 --- .../Magento/Tax/Model/System/Message/NotificationInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php b/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php index 3bb309740cfb1..4f230d1584648 100644 --- a/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php +++ b/app/code/Magento/Tax/Model/System/Message/NotificationInterface.php @@ -10,4 +10,4 @@ */ interface NotificationInterface extends \Magento\Framework\Notification\MessageInterface { -} \ No newline at end of file +} From 85ddb7dba08a28618eacaaec728a569df9656929 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 31 May 2017 09:28:53 +0300 Subject: [PATCH 157/363] MAGETWO-59622: cannot upgrade 2.0 => 2.1 with auto_increment > 1 - for 2.1.x --- app/code/Magento/Cms/Setup/UpgradeData.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Setup/UpgradeData.php b/app/code/Magento/Cms/Setup/UpgradeData.php index 7518960252e80..a960fe32b74d5 100644 --- a/app/code/Magento/Cms/Setup/UpgradeData.php +++ b/app/code/Magento/Cms/Setup/UpgradeData.php @@ -13,6 +13,9 @@ class UpgradeData implements UpgradeDataInterface { + /** + * @deprecated + */ const PRIVACY_COOKIE_PAGE_ID = 4; /** @@ -234,7 +237,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface EOD; - $privacyAndCookiePolicyPage = $this->createPage()->load(self::PRIVACY_COOKIE_PAGE_ID); + $privacyAndCookiePolicyPage = $this->createPage()->load( + 'privacy-policy-cookie-restriction-mode', + 'identifier' + ); $privacyAndCookiePolicyPageId = $privacyAndCookiePolicyPage->getId(); if ($privacyAndCookiePolicyPageId) { $privacyAndCookiePolicyPage->setContent($newPageContent); From acca7cb3d55a5a07c1db8094d3ee116c5e8d03bc Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Wed, 31 May 2017 11:35:09 +0300 Subject: [PATCH 158/363] MAGETWO-58918: Unable to create custom image attribute in category - for 2.1.x --- .../Adminhtml/Category/Image/Upload.php | 5 +- .../Controller/Adminhtml/Category/Save.php | 65 +++- app/code/Magento/Catalog/Model/Category.php | 9 +- .../Category/Attribute/Backend/Image.php | 55 ++- .../Catalog/Model/Category/DataProvider.php | 49 ++- .../Adminhtml/Category/Image/UploadTest.php | 120 +++++++ .../Adminhtml/Category/SaveTest.php | 233 +++++++++--- .../Category/Attribute/Backend/ImageTest.php | 293 +++++++++++++++ .../Catalog/Test/Unit/Model/CategoryTest.php | 333 +++++++++++++----- .../base/web/js/form/element/file-uploader.js | 10 +- .../Magento/Test/Integrity/ClassesTest.php | 10 +- .../Magento/Framework/App/Utility/Classes.php | 3 +- .../Magento/Framework/App/Utility/Files.php | 6 + 13 files changed, 1026 insertions(+), 165 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php index e016e0a2ee84f..689569aefd94c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php @@ -44,14 +44,15 @@ protected function _isAllowed() } /** - * Upload file controller action + * Upload file controller action. * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { + $imageId = $this->_request->getParam('param_name', 'image'); try { - $result = $this->imageUploader->saveFileToTmpDir('image'); + $result = $this->imageUploader->saveFileToTmpDir($imageId); $result['cookie'] = [ 'name' => $this->_getSession()->getName(), diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index 39154f4c24ff1..3dff5e6065fbe 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Category; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\Data\CategoryAttributeInterface; /** * Class Save @@ -48,6 +49,13 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category */ private $storeManager; + /** + * Config instance holder. + * + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * Constructor * @@ -72,8 +80,9 @@ public function __construct( } /** - * Filter category data + * Filter category data. * + * @deprecated * @param array $rawData * @return array */ @@ -126,7 +135,7 @@ public function execute() $this->storeManager->setCurrentStore($store->getCode()); $parentId = isset($categoryPostData['parent']) ? $categoryPostData['parent'] : null; if ($categoryPostData) { - $category->addData($this->_filterCategoryPostData($categoryPostData)); + $category->addData($categoryPostData); if ($isNewCategory) { $parentCategory = $this->getParentCategory($parentId, $storeId); $category->setPath($parentCategory->getPath()); @@ -248,21 +257,47 @@ public function execute() } /** - * Image data preprocessing + * Sets image attribute data to false, if image was removed. * * @param array $data * * @return array */ - public function imagePreprocessing($data) + public function imagePreprocessing(array $data) { - if (empty($data['image'])) { - unset($data['image']); - $data['image']['delete'] = true; + $emptyImageAttributes = $this->getEmptyImageAttributes($data); + foreach ($emptyImageAttributes as $attributeCode => $attributeModel) { + $data[$attributeCode] = false; } + return $data; } + /** + * Get image attributes without value. + * + * @param array $data + * @return array + */ + private function getEmptyImageAttributes(array $data) + { + $result = []; + $entityType = $this->getConfig()->getEntityType(CategoryAttributeInterface::ENTITY_TYPE_CODE); + foreach ($entityType->getAttributeCollection() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $backendModel = $attribute->getBackend(); + if (isset($data[$attributeCode])) { + continue; + } + if (!$backendModel instanceof \Magento\Catalog\Model\Category\Attribute\Backend\Image) { + continue; + } + $result[$attributeCode] = $attribute; + } + + return $result; + } + /** * Converting inputs from string to boolean * @@ -346,4 +381,20 @@ protected function getRedirectParams($isNewCategory, $hasError, $categoryId, $pa } return ['path' => $path, 'params' => $params]; } + + /** + * Get Config instance. + * + * @return \Magento\Eav\Model\Config + */ + private function getConfig() + { + if (null === $this->eavConfig) { + $this->eavConfig = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Eav\Model\Config::class + ); + } + + return $this->eavConfig; + } } diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index d24169bc14ba0..a560e7e58e6de 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -652,14 +652,16 @@ public function formatUrlKey($str) } /** - * Retrieve image URL + * Get image url by attribute code. * + * @param string $attributeCode * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getImageUrl() + public function getImageUrl($attributeCode = 'image') { $url = false; - $image = $this->getImage(); + $image = $this->getData($attributeCode); if ($image) { if (is_string($image)) { $url = $this->_storeManager->getStore()->getBaseUrl( @@ -671,6 +673,7 @@ public function getImageUrl() ); } } + return $url; } diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php index be448053342a0..a8bc510817f1b 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -70,7 +70,45 @@ public function __construct( } /** - * Get image uploader + * Avoiding saving potential upload data to DB. + * Will set empty image attribute value if image was not uploaded. + * + * @param \Magento\Framework\DataObject $object + * @return $this + */ + public function beforeSave($object) + { + $attributeName = $this->getAttribute()->getName(); + $value = $object->getData($attributeName); + $imageName = $this->getUploadedImageName($value); + + if ($imageName) { + $object->setData($attributeName, $imageName); + } else if (!is_string($value)) { + $object->setData($attributeName, ''); + } + + return parent::beforeSave($object); + } + + /** + * Gets image name from $value array. + * Will return empty string in case $value is not an array. + * + * @param array $value Attribute value + * @return string + */ + private function getUploadedImageName($value) + { + if (is_array($value) && isset($value[0]['name'])) { + return $value[0]['name']; + } + + return ''; + } + + /** + * Get image uploader. * * @return \Magento\Catalog\Model\ImageUploader * @@ -79,26 +117,25 @@ public function __construct( private function getImageUploader() { if ($this->imageUploader === null) { - $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Catalog\CategoryImageUpload' - ); + $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\CategoryImageUpload::class); } + return $this->imageUploader; } /** - * Save uploaded file and set its name to category + * Save uploaded file and set its name to category. * * @param \Magento\Framework\DataObject $object * @return \Magento\Catalog\Model\Category\Attribute\Backend\Image */ public function afterSave($object) { - $image = $object->getData($this->getAttribute()->getName(), null); - - if ($image !== null) { + $imageName = $object->getData($this->getAttribute()->getName(), null); + if ($imageName) { try { - $this->getImageUploader()->moveFileFromTmp($image); + $this->getImageUploader()->moveFileFromTmp($imageName); } catch (\Exception $e) { $this->_logger->critical($e); } diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 4b91d0ac68f5f..5ca50bd75b8dd 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -17,6 +17,7 @@ use Magento\Ui\DataProvider\EavValidationRules; use Magento\Catalog\Model\CategoryFactory; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Category\Attribute\Backend\Image as ImageBackendModel; /** * Class DataProvider @@ -206,16 +207,54 @@ public function getData() $categoryData = $this->addUseDefaultSettings($category, $categoryData); $categoryData = $this->addUseConfigSettings($categoryData); $categoryData = $this->filterFields($categoryData); - if (isset($categoryData['image'])) { - unset($categoryData['image']); - $categoryData['image'][0]['name'] = $category->getData('image'); - $categoryData['image'][0]['url'] = $category->getImageUrl(); - } + $categoryData = $this->convertValues($category, $categoryData); + $this->loadedData[$category->getId()] = $categoryData; } return $this->loadedData; } + /** + * Converts category image data to acceptable format for rendering. + * + * @param \Magento\Catalog\Model\Category $category + * @param array $categoryData + * @return array + */ + private function convertValues(Category $category, array $categoryData) + { + $imageAttributes = $this->getImageAttributes($category, $categoryData); + foreach ($imageAttributes as $attributeCode => $attribute) { + unset($categoryData[$attributeCode]); + $categoryData[$attributeCode][0]['name'] = $category->getData($attributeCode); + $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode); + } + + return $categoryData; + } + + /** + * Get all category image attributes. + * + * @param \Magento\Catalog\Model\Category $category + * @param array $categoryData + * @return array + */ + private function getImageAttributes(Category $category, array $categoryData) + { + $imageAttributes = []; + foreach ($category->getAttributes() as $attributeCode => $attribute) { + if (!isset($categoryData[$attributeCode])) { + continue; + } + if ($attribute->getBackend() instanceof ImageBackendModel) { + $imageAttributes[$attributeCode] = $attribute; + } + } + + return $imageAttributes; + } + /** * Get attributes meta * diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php new file mode 100644 index 0000000000000..e4fb125d6c347 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php @@ -0,0 +1,120 @@ +objectManager = new ObjectManager($this); + } + + /** + * Test Uploader::execute() handle request and move image into tmp dir. + * + * @param string $name + * @param string $savedName + * + * @dataProvider executeDataProvider + */ + public function testExecute($name, $savedName) + { + $cookieName = 'testName'; + $sessionId = 'testSessionId'; + $lifetime = 'testLifetime'; + $path = 'testPath'; + $domain = 'testDomain'; + $data = [ + 'cookie' => [ + 'name' => $cookieName, + 'value' => $sessionId, + 'lifetime' => $lifetime, + 'path' => $path, + 'domain' => $domain + ] + ]; + $request = $this->objectManager->getObject(Request::class); + $uploader = $this->getMockBuilder(ImageUploader::class) + ->disableOriginalConstructor() + ->setMethods(['saveFileToTmpDir']) + ->getMock(); + $resultFactory = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $resultFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue(new DataObject())); + $session = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $session->expects($this->once()) + ->method('getName') + ->willReturn($cookieName); + $session->expects($this->once()) + ->method('getSessionId') + ->willReturn($sessionId); + $session->expects($this->once()) + ->method('getCookieLifeTime') + ->willReturn($lifetime); + $session->expects($this->once()) + ->method('getCookiePath') + ->willReturn($path); + $session->expects($this->once()) + ->method('getCookieDomain') + ->willReturn($domain); + $model = $this->objectManager->getObject(Model::class, [ + 'request' => $request, + 'resultFactory' => $resultFactory, + 'imageUploader' => $uploader, + '_session' => $session + ]); + $uploader->expects($this->once()) + ->method('saveFileToTmpDir') + ->with($savedName) + ->will($this->returnValue([])); + $request->setParam('param_name', $name); + $result = $model->execute(); + $this->assertSame($data, $result->getData()); + } + + /** + * Data for testExecute. + * + * @return array + */ + public function executeDataProvider() + { + return [ + ['image1', 'image1'], + ['image2', 'image2'], + [null, 'image'], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php index e46d517e881d5..51b1cde5917b8 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php @@ -3,8 +3,16 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category; +use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Catalog\Controller\Adminhtml\Category\Save; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Type; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManager; + /** * Class SaveTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,67 +22,81 @@ class SaveTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectFactoryMock; + private $resultRedirectFactoryMock; /** * @var \Magento\Framework\Controller\Result\RawFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRawFactoryMock; + private $resultRawFactoryMock; /** * @var \Magento\Framework\Controller\Result\JsonFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultJsonFactoryMock; + private $resultJsonFactoryMock; /** * @var \Magento\Framework\View\LayoutFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $layoutFactoryMock; + private $layoutFactoryMock; /** * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $contextMock; + private $contextMock; /** * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject */ - protected $titleMock; + private $titleMock; /** * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + private $requestMock; /** * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; + private $eventManagerMock; /** * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $responseMock; + private $responseMock; /** * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $objectManager; + private $objectManager; + + /** + * Config mock holder. + * + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavCongig; /** - * @var \Magento\Catalog\Controller\Adminhtml\Category\Save + * StoreManager mock holder. + * + * @var StoreManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var Save */ - protected $save; + private $save; /** * Set up @@ -84,11 +106,10 @@ class SaveTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->markTestSkipped('Due to MAGETWO-48956'); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->contextMock = $this->getMock( - 'Magento\Backend\App\Action\Context', + \Magento\Backend\App\Action\Context::class, [ 'getTitle', 'getRequest', @@ -103,35 +124,35 @@ protected function setUp() false ); $this->resultRedirectFactoryMock = $this->getMock( - 'Magento\Backend\Model\View\Result\RedirectFactory', + \Magento\Backend\Model\View\Result\RedirectFactory::class, ['create'], [], '', false ); $this->resultRawFactoryMock = $this->getMock( - 'Magento\Framework\Controller\Result\RawFactory', + \Magento\Framework\Controller\Result\RawFactory::class, [], [], '', false ); $this->resultJsonFactoryMock = $this->getMock( - 'Magento\Framework\Controller\Result\JsonFactory', + \Magento\Framework\Controller\Result\JsonFactory::class, ['create'], [], '', false ); $this->layoutFactoryMock = $this->getMock( - 'Magento\Framework\View\LayoutFactory', + \Magento\Framework\View\LayoutFactory::class, ['create'], [], '', false ); $this->requestMock = $this->getMockForAbstractClass( - 'Magento\Framework\App\RequestInterface', + \Magento\Framework\App\RequestInterface::class, [], '', false, @@ -139,11 +160,11 @@ protected function setUp() true, ['getParam', 'getPost', 'getPostValue'] ); - $this->objectManagerMock = $this->getMockBuilder('Magento\Framework\ObjectManagerInterface') + $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMock(); $this->eventManagerMock = $this->getMockForAbstractClass( - 'Magento\Framework\Event\ManagerInterface', + \Magento\Framework\Event\ManagerInterface::class, [], '', false, @@ -152,13 +173,13 @@ protected function setUp() ['dispatch'] ); $this->responseMock = $this->getMockForAbstractClass( - 'Magento\Framework\App\ResponseInterface', + \Magento\Framework\App\ResponseInterface::class, [], '', false ); $this->messageManagerMock = $this->getMockForAbstractClass( - 'Magento\Framework\Message\ManagerInterface', + \Magento\Framework\Message\ManagerInterface::class, [], '', false, @@ -177,15 +198,24 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->save = $this->objectManager->getObject( - 'Magento\Catalog\Controller\Adminhtml\Category\Save', + Save::class, [ 'context' => $this->contextMock, 'resultRawFactory' => $this->resultRawFactoryMock, 'resultJsonFactory' => $this->resultJsonFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock + 'layoutFactory' => $this->layoutFactoryMock, + 'storeManager' => $this->storeManager ] ); + $this->eavCongig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManager->setBackwardCompatibleProperty($this->save, 'eavConfig', $this->eavCongig); } /** @@ -217,7 +247,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $resultRedirectMock */ $resultRedirectMock = $this->getMock( - 'Magento\Backend\Model\View\Result\Redirect', + \Magento\Backend\Model\View\Result\Redirect::class, [], [], '', @@ -228,7 +258,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $blockMock */ $blockMock = $this->getMock( - 'Magento\Framework\View\Element\Messages', + \Magento\Framework\View\Element\Messages::class, ['setMessages', 'getGroupedHtml'], [], '', @@ -239,7 +269,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $categoryMock */ $categoryMock = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'setStoreId', 'load', @@ -249,6 +279,7 @@ public function testExecute($categoryId, $storeId, $parentId) 'setParentId', 'setData', 'addData', + 'getAttributes', 'setAttributeSetId', 'getDefaultAttributeSetId', 'getProductsReadonly', @@ -268,7 +299,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $parentCategoryMock */ $parentCategoryMock = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'setStoreId', 'load', @@ -292,7 +323,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $sessionMock */ $sessionMock = $this->getMock( - 'Magento\Backend\Model\Auth\Session', + \Magento\Backend\Model\Auth\Session::class, [], [], '', @@ -303,7 +334,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $registryMock */ $registryMock = $this->getMock( - 'Magento\Framework\Registry', + \Magento\Framework\Registry::class, ['register'], [], '', @@ -314,7 +345,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $wysiwygConfigMock */ $wysiwygConfigMock = $this->getMock( - 'Magento\Cms\Model\Wysiwyg\Config', + \Magento\Cms\Model\Wysiwyg\Config::class, ['setStoreId'], [], '', @@ -325,7 +356,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $storeManagerMock */ $storeManagerMock = $this->getMockForAbstractClass( - 'Magento\Store\Model\StoreManagerInterface', + \Magento\Store\Model\StoreManagerInterface::class, [], '', false, @@ -338,7 +369,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $layoutMock */ $layoutMock = $this->getMockForAbstractClass( - 'Magento\Framework\View\Layout', + \Magento\Framework\View\Layout::class, [], '', false, @@ -351,7 +382,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $resultJsonMock */ $resultJsonMock = $this->getMock( - 'Magento\Cms\Model\Wysiwyg\Config', + \Magento\Cms\Model\Wysiwyg\Config::class, ['setData'], [], '', @@ -362,7 +393,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $messagesMock */ $messagesMock = $this->getMock( - 'Magento\Framework\Message\Collection', + \Magento\Framework\Message\Collection::class, [], [], '', @@ -395,10 +426,10 @@ public function testExecute($categoryId, $storeId, $parentId) ->will( $this->returnValueMap( [ - ['Magento\Backend\Model\Auth\Session', $sessionMock], - ['Magento\Framework\Registry', $registryMock], - ['Magento\Cms\Model\Wysiwyg\Config', $wysiwygConfigMock], - ['Magento\Store\Model\StoreManagerInterface', $storeManagerMock], + [\Magento\Backend\Model\Auth\Session::class, $sessionMock], + [\Magento\Framework\Registry::class, $registryMock], + [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfigMock], + [\Magento\Store\Model\StoreManagerInterface::class, $storeManagerMock], ] ) ); @@ -433,14 +464,15 @@ public function testExecute($categoryId, $storeId, $parentId) ->method('getPostValue') ->willReturn($postData); $addData = $postData; - $addData['image'] = ['delete' => true]; $categoryMock->expects($this->once()) ->method('addData') ->with($addData); $categoryMock->expects($this->any()) ->method('getId') ->will($this->returnValue($categoryId)); - + $categoryMock->expects($this->once()) + ->method('getAttributes') + ->willReturn([]); if (!$parentId) { if ($storeId) { $storeManagerMock->expects($this->once()) @@ -499,7 +531,7 @@ public function testExecute($categoryId, $storeId, $parentId) ); $categoryResource = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Category', + \Magento\Catalog\Model\ResourceModel\Category::class, [], [], '', @@ -537,9 +569,28 @@ public function testExecute($categoryId, $storeId, $parentId) $blockMock->expects($this->once()) ->method('getGroupedHtml') ->will($this->returnValue('grouped-html')); + $entityType = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $entityType->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn([]); $this->resultJsonFactoryMock->expects($this->once()) ->method('create') ->will($this->returnValue($resultJsonMock)); + $this->eavCongig->expects($this->once()) + ->method('getEntityType') + ->with(CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->willReturn($entityType); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->once()) + ->method('getCode') + ->willReturn('testCode'); + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($store); $categoryMock->expects($this->once()) ->method('toArray') ->will($this->returnValue(['category-data'])); @@ -577,4 +628,96 @@ public function dataProviderExecute() ] ]; } + + /** + * Test Save::ImagePreprocessing() does set image attribute data to false if there are no value(image was removed). + * + * @dataProvider imagePreprocessingDataProvider + * @param array $data + * @return void + */ + public function testImagePreprocessingWithoutValue($data) + { + $eavConfig = $this->getMock(\Magento\Eav\Model\Config::class, ['getEntityType'], [], '', false); + $imageBackendModel = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class + ); + $collection = new \Magento\Framework\DataObject([ + 'attribute_collection' => [ + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute1', + 'backend' => $imageBackendModel + ]), + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute2', + 'backend' => new \Magento\Framework\DataObject() + ]) + ] + ]); + $eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->will($this->returnValue($collection)); + $model = $this->objectManager->getObject(Save::class, [ + 'eavConfig' => $eavConfig + ]); + $result = $model->imagePreprocessing($data); + $this->assertEquals([ + 'attribute1' => false, + 'attribute2' => 123 + ], $result); + } + + /** + * Test Save::ImagePreprocessing() doesn't set image attribute data to false if image wasn't removed(value exists). + * + * @return void + */ + public function testImagePreprocessingWithValue() + { + $eavConfig = $this->getMock(\Magento\Eav\Model\Config::class, ['getEntityType'], [], '', false); + $imageBackendModel = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class + ); + $collection = new \Magento\Framework\DataObject([ + 'attribute_collection' => [ + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute1', + 'backend' => $imageBackendModel + ]), + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute2', + 'backend' => new \Magento\Framework\DataObject() + ]) + ] + ]); + $eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->will($this->returnValue($collection)); + $model = $this->objectManager->getObject(Save::class, [ + 'eavConfig' => $eavConfig + ]); + $result = $model->imagePreprocessing([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ]); + $this->assertEquals([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ], $result); + } + + /** + * Test data for testImagePreprocessingWithoutValue. + * + * @return array + */ + public function imagePreprocessingDataProvider() + { + return [ + [['attribute1' => null, 'attribute2' => 123]], + [['attribute2' => 123]] + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php new file mode 100644 index 0000000000000..9ad566cf93e36 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -0,0 +1,293 @@ +objectManager = new ObjectManager($this); + $this->attribute = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + 'TestAttribute', + false, + false, + true, + ['getName'] + ); + $this->attribute->expects($this->once()) + ->method('getName') + ->will($this->returnValue('test_attribute')); + $this->logger = $this->getMockForAbstractClass( + LoggerInterface::class, + [], + 'TestLogger', + false, + false, + true, + ['critical'] + ); + $this->imageUploader = $this->getMockBuilder(ImageUploader::class) + ->disableOriginalConstructor() + ->setMethods(['moveFileFromTmp']) + ->getMock(); + } + + /** + * Test Image::beforeSave() returns empty string on attribute removal. + * + * @dataProvider deletedValueDataProvider + * @param array $value + * @return void + */ + public function testBeforeSaveValueDeletion($value) + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + $model->beforeSave($object); + $this->assertEquals('', $object->getTestAttribute()); + } + + /** + * Test Image::beforeSave() with invalid attribute value returns empty string. + * + * @dataProvider invalidValueDataProvider + * @param array $value + * @return void + */ + public function testBeforeSaveValueInvalid($value) + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + $model->beforeSave($object); + $this->assertEquals('', $object->getTestAttribute()); + } + + /** + * Test Image::beforeSave() save attribute image name. + * + * @return void + */ + public function testBeforeSaveAttributeFileName() + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => [ + ['name' => 'test123.jpg'] + ] + ]); + $model->beforeSave($object); + $this->assertEquals('test123.jpg', $object->getTestAttribute()); + } + + /** + * Test Image::beforeSave() can handle attribute value as string. + * + * @return void + */ + public function testBeforeSaveAttributeStringValue() + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => 'test123.jpg' + ]); + $model->beforeSave($object); + $this->assertEquals('test123.jpg', $object->getTestAttribute()); + } + + /** + * Test Image::afterSave(). + * + * @return void + */ + public function testAfterSave() + { + $model = $this->setUpModelForAfterSave(); + $this->imageUploader->expects($this->once()) + ->method('moveFileFromTmp') + ->with($this->equalTo('test1234.jpg')); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => 'test1234.jpg' + ] + ); + $model->afterSave($object); + } + + /** + * Test Image::afterSave() with invalid attribute value. + * + * @dataProvider invalidValueDataProviderForAfterSave + * @param array $value + * @return void + */ + public function testAfterSaveValueInvalid($value) + { + $model = $this->setUpModelForAfterSave(); + $this->imageUploader->expects($this->never()) + ->method('moveFileFromTmp'); + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => $value + ] + ); + $model->afterSave($object); + } + + /** + * Test Image::afterSave() log error on exception. + * + * @return void + */ + public function testAfterSaveWithExceptions() + { + $model = $this->setUpModelForAfterSave(); + $exception = new \Exception(); + $this->imageUploader->expects($this->any()) + ->method('moveFileFromTmp') + ->will($this->throwException($exception)); + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->equalTo($exception)); + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => 'test1234.jpg' + ] + ); + $model->afterSave($object); + } + + /** + * Prepare Image for Image::afterSave(). + * + * @return Image + */ + private function setUpModelForAfterSave() + { + $objectManagerMock = $this->getMockBuilder(\Magento\Framework\App\ObjectManager::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMock(); + $imageUploaderMock = $this->imageUploader; + + $objectManagerMock->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($class, $params = []) use ($imageUploaderMock) { + if ($class == \Magento\Catalog\CategoryImageUpload::class) { + return $imageUploaderMock; + } + + return $this->objectManager->get($class, $params); + })); + + $model = $this->objectManager->getObject(Image::class, [ + 'objectManager' => $objectManagerMock, + 'logger' => $this->logger + ]); + $this->objectManager->setBackwardCompatibleProperty($model, 'imageUploader', $this->imageUploader); + + return $model->setAttribute($this->attribute); + } + + /** + * Test data for testAfterSaveValueInvalid(). + * + * @return array + */ + public function invalidValueDataProviderForAfterSave() + { + return [ + [''], + [false] + ]; + } + + /** + * Test data for testBeforeSaveValueDeletion. + * + * @return array + */ + public function deletedValueDataProvider() + { + return [ + [false], + [['delete' => true]] + ]; + } + + /** + * Test data for testBeforeSaveValueInvalid. + * + * @return array + */ + public function invalidValueDataProvider() + { + $closure = function () { + return false; + }; + + return [ + [1234], + [true], + [new \stdClass()], + [$closure], + [['a' => 1, 'b' => 2]] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 6aae050b784b2..f363a96e11b82 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Model; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Indexer; /** @@ -16,153 +17,216 @@ */ class CategoryTest extends \PHPUnit_Framework_TestCase { - /** @var \Magento\Catalog\Model\Category */ - protected $category; + /** + * @var \Magento\Catalog\Model\Category + */ + private $category; - /** @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; + /** + * @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; + /** + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManager; - /** @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $cacheManager; + /** + * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheManager; - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registry; + /** + * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $registry; - /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; - /** @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeResource; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeResource; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeFactory; - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepository; + /** + * @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryRepository; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $storeCollectionFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeCollectionFactory; - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $url; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $productCollectionFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactory; - /** @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogConfig; + /** + * @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $catalogConfig; - /** @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject */ - protected $filterManager; + /** + * @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterManager; - /** @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatState; + /** + * @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatState; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatIndexer; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatIndexer; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productIndexer; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIndexer; - /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + /** + * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + /** + * @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlFinder; + + /** + * @var \Magento\Framework\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject + */ + private $resource; - /** @var \Magento\Framework\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject */ - protected $resource; + /** + * @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerRegistry; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $indexerRegistry; + /** + * @var \Magento\Catalog\Api\CategoryAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataServiceMock; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $metadataServiceMock; + private $attributeValueFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $attributeValueFactory; + private $objectManager; + protected function setUp() { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->context = $this->getMock( - 'Magento\Framework\Model\Context', + \Magento\Framework\Model\Context::class, ['getEventDispatcher', 'getCacheManager'], [], '', false ); - - $this->eventManager = $this->getMock('Magento\Framework\Event\ManagerInterface'); + $this->eventManager = $this->getMock(\Magento\Framework\Event\ManagerInterface::class); $this->context->expects($this->any())->method('getEventDispatcher') ->will($this->returnValue($this->eventManager)); - $this->cacheManager = $this->getMock('Magento\Framework\App\CacheInterface'); + $this->cacheManager = $this->getMock(\Magento\Framework\App\CacheInterface::class); $this->context->expects($this->any())->method('getCacheManager') ->will($this->returnValue($this->cacheManager)); - - $this->registry = $this->getMock('Magento\Framework\Registry'); - $this->storeManager = $this->getMock('Magento\Store\Model\StoreManagerInterface'); - $this->categoryTreeResource = $this->getMock('Magento\Catalog\Model\ResourceModel\Category\Tree', [], [], '', false); + $this->registry = $this->getMock(\Magento\Framework\Registry::class); + $this->storeManager = $this->getMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->categoryTreeResource = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Category\Tree::class, + [], + [], + '', + false + ); $this->categoryTreeFactory = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Category\TreeFactory', + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class, ['create'], [], '', false); - $this->categoryRepository = $this->getMock('Magento\Catalog\Api\CategoryRepositoryInterface'); + $this->categoryRepository = $this->getMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); $this->storeCollectionFactory = $this->getMock( - 'Magento\Store\Model\ResourceModel\Store\CollectionFactory', + \Magento\Store\Model\ResourceModel\Store\CollectionFactory::class, ['create'], [], '', false ); - $this->url = $this->getMock('Magento\Framework\UrlInterface'); + $this->url = $this->getMock(\Magento\Framework\UrlInterface::class); $this->productCollectionFactory = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\CollectionFactory', + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create'], [], '', false ); - $this->catalogConfig = $this->getMock('Magento\Catalog\Model\Config', [], [], '', false); + $this->catalogConfig = $this->getMock(\Magento\Catalog\Model\Config::class, [], [], '', false); $this->filterManager = $this->getMock( - 'Magento\Framework\Filter\FilterManager', + \Magento\Framework\Filter\FilterManager::class, ['translitUrl'], [], '', false ); - $this->flatState = $this->getMock('Magento\Catalog\Model\Indexer\Category\Flat\State', [], [], '', false); - $this->flatIndexer = $this->getMock('Magento\Framework\Indexer\IndexerInterface'); - $this->productIndexer = $this->getMock('Magento\Framework\Indexer\IndexerInterface'); + $this->flatState = $this->getMock( + \Magento\Catalog\Model\Indexer\Category\Flat\State::class, + [], + [], + '', + false + ); + $this->flatIndexer = $this->getMock(\Magento\Framework\Indexer\IndexerInterface::class); + $this->productIndexer = $this->getMock(\Magento\Framework\Indexer\IndexerInterface::class); $this->categoryUrlPathGenerator = $this->getMock( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class, [], [], '', false ); - $this->urlFinder = $this->getMock('Magento\UrlRewrite\Model\UrlFinderInterface'); + $this->urlFinder = $this->getMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); $this->resource = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Category', + \Magento\Catalog\Model\ResourceModel\Category::class, [], [], '', false ); - $this->indexerRegistry = $this->getMock('Magento\Framework\Indexer\IndexerRegistry', ['get'], [], '', false); - - $this->metadataServiceMock = $this->getMock('\Magento\Catalog\Api\CategoryAttributeRepositoryInterface'); - $this->attributeValueFactory = $this->getMockBuilder('Magento\Framework\Api\AttributeValueFactory') + $this->indexerRegistry = $this->getMock( + \Magento\Framework\Indexer\IndexerRegistry::class, + ['get'], + [], + '', + false + ); + $this->metadataServiceMock = $this->getMock(\Magento\Catalog\Api\CategoryAttributeRepositoryInterface::class); + $this->attributeValueFactory = $this->getMockBuilder(\Magento\Framework\Api\AttributeValueFactory::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getCategoryModel(); } @@ -187,7 +251,7 @@ public function testMoveWhenCannotFindParentCategory() { $this->markTestIncomplete('MAGETWO-31165'); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -197,7 +261,7 @@ public function testMoveWhenCannotFindParentCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->move(1, 2); @@ -212,7 +276,7 @@ public function testMoveWhenCannotFindParentCategory() public function testMoveWhenCannotFindNewCategory() { $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -223,7 +287,7 @@ public function testMoveWhenCannotFindNewCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->move(1, 2); @@ -239,7 +303,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory() { $this->markTestIncomplete('MAGETWO-31165'); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -250,7 +314,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->setId(5); @@ -266,7 +330,7 @@ public function testMovePrimaryWorkflow() ->with('catalog_category_product') ->will($this->returnValue($indexer)); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -277,7 +341,7 @@ public function testMovePrimaryWorkflow() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->setId(3); @@ -299,10 +363,17 @@ public function testGetUseFlatResourceTrue() $this->assertEquals(true, $category->getUseFlatResource()); } - protected function getCategoryModel() + /** + * Create \Magento\Catalog\Model\Category instance. + * + * @return \Magento\Catalog\Model\Category + */ + private function getCategoryModel() { - return (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - 'Magento\Catalog\Model\Category', + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + /** @var Category $category */ + $category = $objectManager->getObject( + \Magento\Catalog\Model\Category::class, [ 'context' => $this->context, 'registry' => $this->registry, @@ -326,8 +397,15 @@ protected function getCategoryModel() 'customAttributeFactory' => $this->attributeValueFactory, ] ); + + return $category; } + /** + * Test data for testReindexFlatEnabled. + * + * @return array + */ public function reindexFlatEnabledTestDataProvider() { return [ @@ -377,6 +455,11 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec $this->category->reindex(); } + /** + * Test data for testReindexFlatDisabled. + * + * @return array + */ public function reindexFlatDisabledTestDataProvider() { return [ @@ -438,11 +521,11 @@ public function testGetCustomAttributes() { $nameAttributeCode = 'name'; $descriptionAttributeCode = 'description'; - $interfaceAttribute = $this->getMock('\Magento\Framework\Api\MetadataObjectInterface'); + $interfaceAttribute = $this->getMock(\Magento\Framework\Api\MetadataObjectInterface::class); $interfaceAttribute->expects($this->once()) ->method('getAttributeCode') ->willReturn($nameAttributeCode); - $descriptionAttribute = $this->getMock('\Magento\Framework\Api\MetadataObjectInterface'); + $descriptionAttribute = $this->getMock(\Magento\Framework\Api\MetadataObjectInterface::class); $descriptionAttribute->expects($this->once()) ->method('getAttributeCode') ->willReturn($descriptionAttributeCode); @@ -475,4 +558,86 @@ public function testGetCustomAttributes() $this->category->getCustomAttribute($descriptionAttributeCode)->getValue() ); } + + /** + * Test get image url by attribute code. + * + * @param string|bool $value + * @param string|bool $url + * @return void + * + * @dataProvider getImageWithAttributeCodeDataProvider + */ + public function testGetImageWithAttributeCode($value, $url) + { + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMock(); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseUrl']) + ->getMock(); + $storeManager->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + $store->expects($this->any()) + ->method('getBaseUrl') + ->with(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) + ->will($this->returnValue('http://www.example.com/')); + /** @var \Magento\Catalog\Model\Category $model */ + $model = $this->objectManager->getObject( + \Magento\Catalog\Model\Category::class, + [ + 'storeManager' => $storeManager + ] + ); + $model->setData('attribute1', $value); + $result = $model->getImageUrl('attribute1'); + $this->assertEquals($url, $result); + } + + /** + * Test data for testGetImageWithAttributeCode. + * + * @return array + */ + public function getImageWithAttributeCodeDataProvider() + { + return [ + ['testimage', 'http://www.example.com/catalog/category/testimage'], + [false, false] + ]; + } + + /** + * Test get image url without specifying attribute code. + * + * @return void + */ + public function testGetImageWithoutAttributeCode() + { + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMock(); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseUrl']) + ->getMock(); + $storeManager->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + $store->expects($this->any()) + ->method('getBaseUrl') + ->with(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) + ->will($this->returnValue('http://www.example.com/')); + /** @var \Magento\Catalog\Model\Category $model */ + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category::class, [ + 'storeManager' => $storeManager + ]); + $model->setData('image', 'myimage'); + $result = $model->getImageUrl(); + $this->assertEquals('http://www.example.com/catalog/category/myimage', $result); + } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index ce54a311b811f..9e9fa5baec80e 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -299,10 +299,16 @@ define([ */ onBeforeFileUpload: function (e, data) { var file = data.files[0], - allowed = this.isFileAllowed(file); + allowed = this.isFileAllowed(file), + target = $(e.target); if (allowed.passed) { - $(e.target).fileupload('process', data).done(function () { + target.on('fileuploadsend', function (event, postData) { + postData.data.set('param_name', this.paramName); + $(event.currentTarget).off('fileuploadsend'); + }.bind(data)); + + target.fileupload('process', data).done(function () { data.submit(); }); } else { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 1d19b7e35c4e0..dd33dfbc73123 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -169,6 +169,7 @@ protected function _assertClassesExist($classes, $path) $badClasses = []; $badUsages = []; foreach ($classes as $class) { + $class = trim($class, '\\'); try { if (strrchr($class, '\\') === false and !Classes::isVirtual($class)) { $badUsages[] = $class; @@ -188,7 +189,7 @@ protected function _assertClassesExist($classes, $path) } self::$_existingClasses[$class] = 1; } catch (\PHPUnit_Framework_AssertionFailedError $e) { - $badClasses[] = $class; + $badClasses[] = '\\' . $class; } } if ($badClasses) { @@ -598,13 +599,8 @@ public function testCoversAnnotation() { $files = Files::init(); $errors = []; - $filesToTest = $files->getPhpFiles(Files::INCLUDE_TESTS); - if (($key = array_search(__FILE__, $filesToTest)) !== false) { - unset($filesToTest[$key]); - } - - foreach ($filesToTest as $file) { + foreach ($files->getFiles([BP . '/dev/tests/{integration,unit}'], '*') as $file) { $code = file_get_contents($file); if (preg_match('/@covers(DefaultClass)?\s+([\w\\\\]+)(::([\w\\\\]+))?/', $code, $matches)) { if ($this->isNonexistentEntityCovered($matches)) { diff --git a/lib/internal/Magento/Framework/App/Utility/Classes.php b/lib/internal/Magento/Framework/App/Utility/Classes.php index b698ab2ba0d57..6723bf4bd2b4d 100644 --- a/lib/internal/Magento/Framework/App/Utility/Classes.php +++ b/lib/internal/Magento/Framework/App/Utility/Classes.php @@ -273,10 +273,11 @@ public static function isAutogenerated($className) { if ( preg_match( - '/.*\\\\[a-zA-Z0-9]{1,}(Factory|Proxy|SearchResults|DataBuilder|Extension|ExtensionInterface)$/', + '/.*\\\\[a-zA-Z0-9]{1,}(Factory|SearchResults|DataBuilder|Extension|ExtensionInterface)$/', $className ) || preg_match('/Magento\\\\[\w]+\\\\(Test\\\\(Page|Fixture))\\\\/', $className) + || preg_match('/.*\\\\[a-zA-Z0-9]{1,}\\\\Proxy$/', $className) ) { return true; } diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 4c22e3ae155b1..7b7c3cd1e225b 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -1277,6 +1277,8 @@ public function classFileExists($class, &$path = '') '/dev/tests/static/framework', '/dev/tests/static/testsuite', '/dev/tests/functional/tests/app', + '/dev/tests/functional/lib', + '/dev/tests/functional/vendor/magento/mtf', '/setup/src' ]; foreach ($directories as $key => $dir) { @@ -1305,6 +1307,10 @@ public function classFileExists($class, &$path = '') if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { return true; } + $trimmedFullPath = $dir . '/' . $classParts[2] . '/' . $classParts[3]; + if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { + return true; + } } } return false; From 364972a50f29e0646a0c8bb68950291bf1134645 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 31 May 2017 14:04:25 +0300 Subject: [PATCH 159/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Product/Form/Modifier/EavTest.php | 245 +++++++++++++++-- .../Product/Form/Modifier/Eav.php | 13 +- .../Entity/Attribute/AbstractAttribute.php | 41 ++- .../Eav/Model/ResourceModel/UpdateHandler.php | 95 +++---- .../view/base/web/js/form/element/select.js | 19 +- .../Product/Form/Modifier/EavTest.php | 15 ++ .../_files/eav_expected_meta_output.php | 1 - .../eav_expected_meta_output_w_default.php | 105 ++++++++ .../Catalog/_files/dropdown_attribute.php | 59 ++++ .../Model/ResourceModel/UpdateHandlerTest.php | 255 ++++++++++++++++++ 10 files changed, 774 insertions(+), 74 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output_w_default.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index d9aa1e32b044d..646e7c2125bf5 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -5,33 +5,36 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory as EavAttributeFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; +use Magento\Eav\Api\Data\AttributeGroupInterface; use Magento\Eav\Model\Config; -use Magento\Framework\App\RequestInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Ui\DataProvider\EavValidationRules; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection as GroupCollection; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; use Magento\Eav\Model\Entity\Attribute\Group; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Eav\Model\Entity\Type as EntityType; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; -use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; -use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection as GroupCollection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; +use Magento\Framework\Api\AttributeInterface; use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\SortOrderBuilder; -use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Api\Data\AttributeGroupInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Currency; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Locale\Currency as CurrencyLocale; -Use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; +use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; /** * Class EavTest @@ -157,6 +160,26 @@ class EavTest extends AbstractModifierTest */ protected $currencyLocaleMock; + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttributeMock; + + /** + * @var ArrayManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $arrayManagerMock; + + /** + * @var EavAttributeFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavAttributeFactoryMock; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; + /** * @var ObjectManager */ @@ -167,6 +190,9 @@ class EavTest extends AbstractModifierTest */ protected $eav; + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { parent::setUp(); @@ -228,10 +254,24 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->disableOriginalConstructor() + ->getMock(); + $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) + ->getMock(); + $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) + ->getMock(); + $this->eavAttributeFactoryMock = $this->getMockBuilder(EavAttributeFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->eavAttributeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->eavAttributeMock); $this->groupCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->groupCollectionMock); @@ -277,7 +317,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getCurrency']) ->getMock(); - + $this->eavAttributeMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->eav =$this->getModel(); $this->objectManager->setBackwardCompatibleProperty( $this->eav, @@ -304,6 +347,9 @@ protected function createModel() 'attributeGroupRepository' => $this->attributeGroupRepositoryMock, 'sortOrderBuilder' => $this->sortOrderBuilderMock, 'attributeRepository' => $this->attributeRepositoryMock, + 'arrayManager' => $this->arrayManagerMock, + 'eavAttributeFactory' => $this->eavAttributeFactoryMock, + '_eventManager' => $this->eventManagerMock ]); } @@ -399,4 +445,163 @@ public function testModifyData() $this->assertEquals($sourceData, $this->eav->modifyData([])); } + + + /** + * @param int $productId + * @param bool $productRequired + * @param string $attrValue + * @param array $expected + * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists + * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta + * @dataProvider setupAttributeMetaDataProvider + */ + public function testSetupAttributeMetaDefaultAttribute($productId, $productRequired, $attrValue, $expected) + { + $configPath = 'arguments/data/config'; + $groupCode = 'product-details'; + $sortOrder = '0'; + + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($productId); + + $this->productAttributeMock->expects($this->any()) + ->method('getIsRequired') + ->willReturn($productRequired); + + $this->productAttributeMock->expects($this->any()) + ->method('getDefaultValue') + ->willReturn('required_value'); + + $this->productAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('code'); + + $this->productAttributeMock->expects($this->any()) + ->method('getValue') + ->willReturn('value'); + + $attributeMock = $this->getMockBuilder(AttributeInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $attributeMock->expects($this->any()) + ->method('getValue') + ->willReturn($attrValue); + + $this->productMock->expects($this->any()) + ->method('getCustomAttribute') + ->willReturn($attributeMock); + + $this->arrayManagerMock->expects($this->any()) + ->method('set') + ->with( + $configPath, + [], + $expected + ) + ->willReturn($expected); + + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturn($expected); + + $this->arrayManagerMock->expects($this->any()) + ->method('get') + ->willReturn([]); + + $this->arrayManagerMock->expects($this->any()) + ->method('exists'); + + $this->assertEquals( + $expected, + $this->eav->setupAttributeMeta($this->productAttributeMock, $groupCode, $sortOrder) + ); + } + + /** + * @return array + */ + public function setupAttributeMetaDataProvider() + { + return [ + 'default_null_prod_not_new_and_required' => [ + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0 + ], + ], + 'default_null_prod_not_new_and_not_required' => [ + 'productId' => 1, + 'productRequired' => false, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => null, + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0 + ], + ], + 'default_null_prod_new_and_not_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0 + ], + ], + 'default_null_prod_new_and_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0 + ], + ] + ]; + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index ce3dc1a97da91..e240f9771bed7 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -524,6 +524,16 @@ private function getPreviousSetAttributes() return $this->prevSetAttributes; } + /** + * Check is product already new or we trying to create one. + * + * @return bool + */ + private function isProductExists() + { + return (bool) $this->locator->getProduct()->getId(); + } + /** * Initial meta setup * @@ -533,6 +543,7 @@ private function getPreviousSetAttributes() * @return array * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * @api */ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder) @@ -545,7 +556,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'visible' => $attribute->getIsVisible(), 'required' => $attribute->getIsRequired(), 'notice' => $attribute->getNote(), - 'default' => $attribute->getDefaultValue(), + 'default' => (!$this->isProductExists()) ? $attribute->getDefaultValue() : null, 'label' => $attribute->getDefaultFrontendLabel(), 'code' => $attribute->getAttributeCode(), 'source' => $groupCode, diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 96cd1f8b19525..a6dca137b9f7d 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -111,6 +111,20 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens */ protected $dataObjectHelper; + /** + * Array of attribute types that have empty string as a possible value. + * + * @var array + */ + private static $emptyStringTypes = [ + 'int', + 'decimal', + 'datetime', + 'varchar', + 'text', + 'static' + ]; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -588,17 +602,38 @@ protected function _getDefaultSourceModel() } /** + * Check if Value is empty. + * * @param array|null|bool|int|float|string $value * @return bool */ public function isValueEmpty($value) { - /** @var array $emptyStringTypes list of attribute types that treat empty string as a possible value */ - $emptyStringTypes = ['int', 'decimal', 'datetime', 'varchar', 'text', 'static']; return (is_array($value) && count($value) == 0) || $value === null || ($value === false && $this->getBackend()->getType() != 'int') - || ($value === '' && in_array($this->getBackend()->getType(), $emptyStringTypes)); + || ($value === '' && $this->isInEmptyStringTypes()); + } + + /** + * Check if attribute empty value is valid. + * + * @param array|null|bool|int|float|string $value + * @return bool + */ + public function isAllowedEmptyTextValue($value) + { + return $this->isInEmptyStringTypes() && $value === ''; + } + + /** + * Check is attribute type in allowed empty string types. + * + * @return bool + */ + private function isInEmptyStringTypes() + { + return in_array($this->getBackend()->getType(), self::$emptyStringTypes); } /** diff --git a/app/code/Magento/Eav/Model/ResourceModel/UpdateHandler.php b/app/code/Magento/Eav/Model/ResourceModel/UpdateHandler.php index c91aab658d2c2..c45e0e54dd807 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/UpdateHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/UpdateHandler.php @@ -5,12 +5,12 @@ */ namespace Magento\Eav\Model\ResourceModel; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Eav\Api\AttributeRepositoryInterface as AttributeRepository; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Model\Entity\ScopeResolver; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; +use Magento\Framework\Model\Entity\ScopeResolver; /** * Class UpdateHandler @@ -125,51 +125,54 @@ public function execute($entityType, $entityData, $arguments = []) : null; // @todo verify is it normal to not have attributer_set_id $snapshot = $this->readSnapshot->execute($entityType, $entityDataForSnapshot); foreach ($this->getAttributes($entityType, $attributeSetId) as $attribute) { - if ($attribute->isStatic()) { - continue; - } - /** - * Only scalar values can be stored in generic tables - */ - if (isset($entityData[$attribute->getAttributeCode()]) - && !is_scalar($entityData[$attribute->getAttributeCode()])) { + $code = $attribute->getAttributeCode(); + $isAllowedValueType = array_key_exists($code, $entityData) + && (is_scalar($entityData[$code]) || $entityData[$code] === null); + + if ($attribute->isStatic() || !$isAllowedValueType) { continue; } - if (isset($snapshot[$attribute->getAttributeCode()]) - && $snapshot[$attribute->getAttributeCode()] !== false - && (array_key_exists($attribute->getAttributeCode(), $entityData) - && $attribute->isValueEmpty($entityData[$attribute->getAttributeCode()])) - ) { - $this->attributePersistor->registerDelete( - $entityType, - $entityData[$metadata->getLinkField()], - $attribute->getAttributeCode() - ); - } - if ((!array_key_exists($attribute->getAttributeCode(), $snapshot) - || $snapshot[$attribute->getAttributeCode()] === false) - && array_key_exists($attribute->getAttributeCode(), $entityData) - && !$attribute->isValueEmpty($entityData[$attribute->getAttributeCode()]) - ) { - $this->attributePersistor->registerInsert( - $entityType, - $entityData[$metadata->getLinkField()], - $attribute->getAttributeCode(), - $entityData[$attribute->getAttributeCode()] - ); - } - if (array_key_exists($attribute->getAttributeCode(), $snapshot) - && $snapshot[$attribute->getAttributeCode()] !== false - && array_key_exists($attribute->getAttributeCode(), $entityData) - && $snapshot[$attribute->getAttributeCode()] != $entityData[$attribute->getAttributeCode()] - && !$attribute->isValueEmpty($entityData[$attribute->getAttributeCode()]) - ) { - $this->attributePersistor->registerUpdate( - $entityType, - $entityData[$metadata->getLinkField()], - $attribute->getAttributeCode(), - $entityData[$attribute->getAttributeCode()] - ); + + $newValue = $entityData[$code]; + $isValueEmpty = $attribute->isValueEmpty($newValue); + $isAllowedEmptyStringValue = $attribute->isAllowedEmptyTextValue($newValue); + + if (array_key_exists($code, $snapshot)) { + $snapshotValue = $snapshot[$code]; + /** 'FALSE' value for attributes can't be update or delete */ + if ($snapshotValue === false) { + continue; + } + + if (!$isValueEmpty || $isAllowedEmptyStringValue) { + /** NOT Updated value for attributes not need to update */ + if ($snapshotValue === $newValue) { + continue; + } + + $this->attributePersistor->registerUpdate( + $entityType, + $entityData[$metadata->getLinkField()], + $code, + $newValue + ); + } else { + $this->attributePersistor->registerDelete( + $entityType, + $entityData[$metadata->getLinkField()], + $code + ); + } + } else { + /** Only not empty value of attribute is insertable */ + if (!$isValueEmpty || $isAllowedEmptyStringValue) { + $this->attributePersistor->registerInsert( + $entityType, + $entityData[$metadata->getLinkField()], + $code, + $newValue + ); + } } } $this->attributePersistor->flush($entityType, $context); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/select.js b/app/code/Magento/Ui/view/base/web/js/form/element/select.js index fa709217585ce..1c3c8fe947ef7 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/select.js @@ -44,9 +44,9 @@ define([ if (_.isUndefined(caption)) { caption = node.label; } - } else { - return node; } + + return node; }); return { @@ -117,7 +117,7 @@ define([ this._super(); if (this.customEntry) { - this.initInput(); + registry.get(this.name, this.initInput.bind(this)); } if (this.filterBy) { @@ -301,6 +301,19 @@ define([ this.value(value); return this; + }, + + /** + * Initializes observable properties of instance. + * + * @returns {Object} Chainable. + */ + setInitialValue: function () { + if (_.isUndefined(this.value()) && !this.default) { + this.clear(); + } + + return this._super(); } }); }); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index cb671748cd2b5..e64a4d3e26c0a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -70,6 +70,21 @@ public function testModifyMeta() $this->prepareDataForComparison($actualMeta, $expectedMeta); $this->assertEquals($expectedMeta, $actualMeta); } + /* + * Test modifying meta on new product. + */ + public function testModifyMetaNewProduct() + { + $this->objectManager->get(\Magento\Eav\Model\Entity\AttributeCache::class)->clear(); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product->setAttributeSetId(4); + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); + $expectedMeta = include __DIR__ . '/_files/eav_expected_meta_output_w_default.php'; + $actualMeta = $this->eavModifier->modifyMeta([]); + $this->prepareDataForComparison($actualMeta, $expectedMeta); + $this->assertEquals($expectedMeta, $actualMeta); + } /** * @magentoDataFixture Magento/Catalog/_files/product_simple_with_admin_store.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php index 4571e268b4596..5a4aa8971f8e5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php @@ -35,7 +35,6 @@ "visible" => "1", "required" => "0", "label" => "Enable Product", - "default" => "1", "source" => "product-details", "scopeLabel" => "[WEBSITE]", "globalScope" => false, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output_w_default.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output_w_default.php new file mode 100644 index 0000000000000..8ba3183a084f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output_w_default.php @@ -0,0 +1,105 @@ + [ + "arguments" => [ + "data" => [ + "config" => [ + "dataScope" => "data.product", + ], + ], + ], + "children" => [ + "container_status" => [ + "children" => [ + "status" => [ + "arguments" => [ + "data" => [ + "config" => [ + "dataType" => "select", + "formElement" => "select", + "options" => [ + [ + "value" => 1, + "label" => "Enabled" + ], + [ + "value" => 2, + "label" => "Disabled" + ] + ], + "visible" => "1", + "required" => "0", + "label" => "Enable Product", + "default" => "1", + "source" => "product-details", + "scopeLabel" => "[WEBSITE]", + "globalScope" => false, + "code" => "status", + "sortOrder" => "__placeholder__", + "componentType" => "field" + ], + ], + ], + ], + ], + ], + "container_name" => [ + "children" => [ + "name" => [ + "arguments" => [ + "data" => [ + "config" => [ + "dataType" => "text", + "formElement" => "input", + "visible" => "1", + "required" => "1", + "label" => "Product Name", + "source" => "product-details", + "scopeLabel" => "[STORE VIEW]", + "globalScope" => false, + "code" => "name", + "sortOrder" => "__placeholder__", + "componentType" => "field", + "validation" => [ + "required-entry" => true + ], + ], + ], + ], + ], + ], + ], + "container_sku" => [ + "children" => [ + "sku" => [ + "arguments" => [ + "data" => [ + "config" => [ + "dataType" => "text", + "formElement" => "input", + "visible" => "1", + "required" => "1", + "label" => "SKU", + "source" => "product-details", + "scopeLabel" => "[GLOBAL]", + "globalScope" => true, + "code" => "sku", + "sortOrder" => "__placeholder__", + "componentType" => "field", + "validation" => [ + "required-entry" => true + ], + ], + ], + ], + ], + ], + ], + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php new file mode 100644 index 0000000000000..884fe2be81c5b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php @@ -0,0 +1,59 @@ +create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); + +if (!$attribute->loadByCode(4, 'dropdown_attribute')->getId()) { + /** @var $installer \Magento\Catalog\Setup\CategorySetup */ + $installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class + ); + + $attribute->setData( + [ + 'attribute_code' => 'dropdown_attribute', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Drop-Down Attribute'], + 'backend_type' => 'varchar', + 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, + 'option' => [ + 'value' => [ + 'option_1' => ['Option 1'], + 'option_2' => ['Option 2'], + 'option_3' => ['Option 3'], + ], + 'order' => [ + 'option_1' => 1, + 'option_2' => 2, + 'option_3' => 3, + ], + ], + ] + ); + $attribute->save(); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php new file mode 100644 index 0000000000000..55814b92fab7f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php @@ -0,0 +1,255 @@ +create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId(0); + $entity->load(1); + $entity->setData($code, $snapshotValue); + $entity->save(); + } + + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId(0); + $entity->load(1); + + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $entityData = array_merge($entity->getData(), [$code => $newValue]); + $updateHandler->execute(\Magento\Catalog\Api\Data\ProductInterface::class, $entityData); + + $resultEntity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $resultEntity->setStoreId(0); + $resultEntity->load(1); + + $this->assertSame($expected, $resultEntity->getData($code)); + } + + /** + * @covers \Magento\Eav\Model\ResourceModel\UpdateHandlerTest::execute + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider getCustomStoreDataProvider + * @param $code + * @param $snapshotValue + * @param $newValue + * @param $expected + */ + public function testExecuteProcessForCustomStore($code, $snapshotValue, $newValue, $expected) + { + $store = Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store->load('fixture_second_store', 'code'); + + Bootstrap::getObjectManager() + ->create(\Magento\CatalogSearch\Model\Indexer\Fulltext\Processor::class) + ->reindexAll(); + + if ($snapshotValue !== '-') { + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId($store->getId()); + $entity->load(1); + $entity->setData($code, $snapshotValue); + $entity->save(); + } + + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId($store->getId()); + $entity->load(1); + + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $entityData = array_merge($entity->getData(), [$code => $newValue]); + $updateHandler->execute(\Magento\Catalog\Api\Data\ProductInterface::class, $entityData); + + $resultEntity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $resultEntity->setStoreId($store->getId()); + $resultEntity->load(1); + + $this->assertSame($expected, $resultEntity->getData($code)); + } + + /** + * @covers \Magento\Eav\Model\ResourceModel\UpdateHandlerTest::execute + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider getCustomAttributeDataProvider + * @param $code + * @param $defaultStoreValue + * @param $snapshotValue + * @param $newValue + * @param $expected + */ + public function testExecuteProcessForCustomAttributeInCustomStore( + $code, + $defaultStoreValue, + $snapshotValue, + $newValue, + $expected + ) { + $store = Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store->load('fixture_second_store', 'code'); + + Bootstrap::getObjectManager() + ->create(\Magento\CatalogSearch\Model\Indexer\Fulltext\Processor::class) + ->reindexAll(); + + $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + $attribute->loadByCode(4, $code); + + $options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class + ); + $options->setAttributeFilter($attribute->getId()); + $optionIds = $options->getAllIds(); + + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId(0); + $entity->load(1); + $entity->setData($code, $optionIds[$defaultStoreValue]); + $entity->save(); + + if ($snapshotValue !== '-') { + /** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId($store->getId()); + $entity->load(1); + + if ($snapshotValue) { + $snapshotValue = $optionIds[$snapshotValue]; + } + + $entity->setData($code, $snapshotValue); + $entity->save(); + } + + $entity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $entity->setStoreId($store->getId()); + $entity->load(1); + + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + + if ($newValue) { + $newValue = $optionIds[$newValue]; + } + + $entityData = array_merge($entity->getData(), [$code => $newValue]); + $updateHandler->execute(\Magento\Catalog\Api\Data\ProductInterface::class, $entityData); + + $resultEntity = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $resultEntity->setStoreId($store->getId()); + $resultEntity->load(1); + + if ($expected !== null) { + $expected = $optionIds[$expected]; + } + + $this->assertSame($expected, $resultEntity->getData($code)); + } + + /** + * @return array + */ + public function getAllStoresDataProvider() + { + return [ + ['description', '', 'not_empty_value', 'not_empty_value'], //0 + ['description', '', '', null], //1 + ['description', '', null, null], //2 + ['description', '', false, null], //3 + + ['description', 'not_empty_value', 'not_empty_value2', 'not_empty_value2'], //4 + ['description', 'not_empty_value', '', null], //5 + ['description', 'not_empty_value', null, null], //6 + ['description', 'not_empty_value', false, null], //7 + + ['description', null, 'not_empty_value', 'not_empty_value'], //8 + ['description', null, '', null], //9 + ['description', null, false, null], //10 + + ['description', false, 'not_empty_value', 'not_empty_value'], //11 + ['description', false, '', null], //12 + ['description', false, null, null], //13 + ]; + } + + /** + * @return array + */ + public function getCustomStoreDataProvider() + { + return [ + ['description', '', 'not_empty_value', 'not_empty_value'], //0 + ['description', '', '', null], //1 + ['description', '', null, 'Description with html tag'], //2 + ['description', '', false, 'Description with html tag'], //3 + + ['description', 'not_empty_value', 'not_empty_value2', 'not_empty_value2'], //4 + ['description', 'not_empty_value', '', null], //5 + ['description', 'not_empty_value', null, 'Description with html tag'], //6 + ['description', 'not_empty_value', false, 'Description with html tag'], //7 + + ['description', null, 'not_empty_value', 'not_empty_value'], //8 + ['description', null, '', null], //9 + ['description', null, false, 'Description with html tag'], //10 + + ['description', false, 'not_empty_value', 'not_empty_value'], //11 + ['description', false, '', null], //12 + ['description', false, null, 'Description with html tag'], //13 + ]; + } + + /** + * @return array + */ + public function getCustomAttributeDataProvider() + { + return [ + ['dropdown_attribute', 0, '', 1, 1], //0 + ['dropdown_attribute', 0, '', '', null], //1 + ['dropdown_attribute', 0, '', null, 0], //2 + ['dropdown_attribute', 0, '', false, 0], //3 + + ['dropdown_attribute', 0, 1, 2, 2], //4 + ['dropdown_attribute', 0, 1, '', null], //5 + ['dropdown_attribute', 0, 1, null, 0], //6 + ['dropdown_attribute', 0, 1, false, 0], //7 + + ['dropdown_attribute', 0, null, 1, 1], //8 + ['dropdown_attribute', 0, null, '', null], //9 + ['dropdown_attribute', 0, null, false, 0], //10 + + ['dropdown_attribute', 0, false, 1, 1], //11 + ['dropdown_attribute', 0, false, '', null], //12 + ['dropdown_attribute', 0, false, null, 0], //13 + + ['dropdown_attribute', 0, '-', 1, 1], //14 + ['dropdown_attribute', 0, '-', '', null], //15 + ['dropdown_attribute', 0, '-', null, 0], //16 + ['dropdown_attribute', 0, '-', false, 0], //17 + ]; + } +} From b95691ea4f91adf3421d55a806df8ff19581ab28 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Wed, 31 May 2017 14:48:13 +0300 Subject: [PATCH 160/363] MAGETWO-58918: Unable to create custom image attribute in category - for 2.1.x --- .../Controller/Adminhtml/Category/Save.php | 3 +- .../Catalog/Model/Category/DataProvider.php | 3 +- .../Magento/Framework/App/Utility/Files.php | 69 +++++++++++-------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index 3dff5e6065fbe..256e5b9760f1a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -266,7 +266,8 @@ public function execute() public function imagePreprocessing(array $data) { $emptyImageAttributes = $this->getEmptyImageAttributes($data); - foreach ($emptyImageAttributes as $attributeCode => $attributeModel) { + $attributeCodes = array_keys($emptyImageAttributes); + foreach ($attributeCodes as $attributeCode) { $data[$attributeCode] = false; } diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 5ca50bd75b8dd..3fdb5020a6cc8 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -224,7 +224,8 @@ public function getData() private function convertValues(Category $category, array $categoryData) { $imageAttributes = $this->getImageAttributes($category, $categoryData); - foreach ($imageAttributes as $attributeCode => $attribute) { + $attributeCodes = array_keys($imageAttributes); + foreach ($attributeCodes as $attributeCode) { unset($categoryData[$attributeCode]); $categoryData[$attributeCode][0]['name'] = $category->getData($attributeCode); $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode); diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 7b7c3cd1e225b..b694cd9415de5 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -1287,33 +1287,7 @@ public function classFileExists($class, &$path = '') $directories = array_merge($directories, $this->getPaths()); - foreach ($directories as $dir) { - $fullPath = $dir . '/' . $path; - if ($this->classFileExistsCheckContent($fullPath, $namespace, $className)) { - return true; - } - $classParts = explode('/', $path, 3); - if (count($classParts) >= 3) { - // Check if it's PSR-4 class with trimmed vendor and package name parts - $trimmedFullPath = $dir . '/' . $classParts[2]; - if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { - return true; - } - } - $classParts = explode('/', $path, 4); - if (count($classParts) >= 4) { - // Check if it's a library under framework directory - $trimmedFullPath = $dir . '/' . $classParts[3]; - if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { - return true; - } - $trimmedFullPath = $dir . '/' . $classParts[2] . '/' . $classParts[3]; - if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { - return true; - } - } - } - return false; + return $this->checkDirectories($path, $directories, $namespace, $className); } /** @@ -1555,4 +1529,45 @@ protected function getFilesSubset(array $dirPatterns, $fileNamePattern, $exclude } return $fileSet; } + + /** + * Check class exists in searchable directories. + * + * @param string $path + * @param array $directories + * @param string $namespace + * @param string $className + * @return bool + */ + private function checkDirectories(&$path, $directories, $namespace, $className) + { + foreach ($directories as $dir) { + $fullPath = $dir . '/' . $path; + if ($this->classFileExistsCheckContent($fullPath, $namespace, $className)) { + return true; + } + $classParts = explode('/', $path, 3); + if (count($classParts) >= 3) { + // Check if it's PSR-4 class with trimmed vendor and package name parts + $trimmedFullPath = $dir . '/' . $classParts[2]; + if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { + return true; + } + } + $classParts = explode('/', $path, 4); + if (count($classParts) >= 4) { + // Check if it's a library under framework directory + $trimmedFullPath = $dir . '/' . $classParts[3]; + if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { + return true; + } + $trimmedFullPath = $dir . '/' . $classParts[2] . '/' . $classParts[3]; + if ($this->classFileExistsCheckContent($trimmedFullPath, $namespace, $className)) { + return true; + } + } + } + + return false; + } } From 8beb099d9982e990d82b25e88e97e7997bed0742 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 31 May 2017 16:15:49 +0300 Subject: [PATCH 161/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../DataProvider/Product/Form/Modifier/EavTest.php | 1 - .../Catalog/Controller/Adminhtml/CategoryTest.php | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 646e7c2125bf5..388672daf1e99 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -446,7 +446,6 @@ public function testModifyData() $this->assertEquals($sourceData, $this->eav->modifyData([])); } - /** * @param int $productId * @param bool $productRequired diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 150c8a84c4fe8..1fa4fc182eab4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -23,7 +23,7 @@ class CategoryTest extends \Magento\TestFramework\TestCase\AbstractBackendContro public function testSaveAction($inputData, $defaultAttributes, $attributesSaved = [], $isSuccess = true) { /** @var $store \Magento\Store\Model\Store */ - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Store\Model\Store'); + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); $store->load('fixturestore', 'code'); $storeId = $store->getId(); @@ -41,7 +41,7 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved /** @var $category \Magento\Catalog\Model\Category */ $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Category' + \Magento\Catalog\Model\Category::class ); $category->setStoreId($storeId); $category->load(2); @@ -86,7 +86,7 @@ public function testSaveActionFromProductCreationPage($postData) ); } else { $result = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\Json\Helper\Data' + \Magento\Framework\Json\Helper\Data::class )->jsonDecode( $body ); @@ -233,7 +233,7 @@ public function saveActionDataProvider() 'display_mode' => true, 'meta_title' => true, 'custom_design' => true, - 'page_layout' => false, + 'page_layout' => true, 'is_active' => true, 'include_in_menu' => true, 'landing_page' => true, @@ -242,7 +242,7 @@ public function saveActionDataProvider() 'description' => true, 'meta_keywords' => true, 'meta_description' => true, - 'custom_layout_update' => false, + 'custom_layout_update' => true, 'custom_design_from' => true, 'custom_design_to' => true, 'filter_price_range' => false @@ -364,7 +364,7 @@ public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, foreach ($urlKeys as $categoryId => $urlKey) { /** @var $category \Magento\Catalog\Model\Category */ $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Category' + \Magento\Catalog\Model\Category::class ); if ($categoryId > 0) { $category->load($categoryId) From b84fe279c2306d74441354899900dc9f124437b6 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Wed, 31 May 2017 16:28:19 +0300 Subject: [PATCH 162/363] MAGETWO-56988: [Backport] - [GitHub] No Swatch Input for Admin Scope. No Fallback to Admin Scope #5143 #5142 - for 2.1 --- .../Unit/Model/Plugin/EavAttributeTest.php | 78 ++++++++++++++++--- app/code/Magento/Swatches/i18n/en_US.csv | 1 + .../catalog/product/attribute/text.phtml | 22 ++---- 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php index 3d4e5bbee1ab9..b9f69bb0ec332 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php @@ -56,22 +56,22 @@ class EavAttributeTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->attribute = $this->getMock('\Magento\Catalog\Model\ResourceModel\Eav\Attribute', [], [], '', false); - $this->swatchFactory = $this->getMock('\Magento\Swatches\Model\SwatchFactory', ['create'], [], '', false); - $this->swatchHelper = $this->getMock('\Magento\Swatches\Helper\Data', [], [], '', false); - $this->swatch = $this->getMock('\Magento\Swatches\Model\Swatch', [], [], '', false); - $this->resource = $this->getMock('Magento\Swatches\Model\ResourceModel\Swatch', [], [], '', false); + $this->attribute = $this->getMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], [], '', false); + $this->swatchFactory = $this->getMock(\Magento\Swatches\Model\SwatchFactory::class, ['create'], [], '', false); + $this->swatchHelper = $this->getMock(\Magento\Swatches\Helper\Data::class, [], [], '', false); + $this->swatch = $this->getMock(\Magento\Swatches\Model\Swatch::class, [], [], '', false); + $this->resource = $this->getMock(\Magento\Swatches\Model\ResourceModel\Swatch::class, [], [], '', false); $this->collection = - $this->getMock('\Magento\Swatches\Model\ResourceModel\Swatch\Collection', [], [], '', false); + $this->getMock(\Magento\Swatches\Model\ResourceModel\Swatch\Collection::class, [], [], '', false); $this->collectionFactory = $this->getMock( - '\Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory', + \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory::class, ['create'], [], '', false ); $this->abstractSource = $this->getMock( - '\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource', + \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class, [], [], '', @@ -80,7 +80,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->eavAttribute = $objectManager->getObject( - 'Magento\Swatches\Model\Plugin\EavAttribute', + \Magento\Swatches\Model\Plugin\EavAttribute::class, [ 'collectionFactory' => $this->collectionFactory, 'swatchFactory' => $this->swatchFactory, @@ -378,6 +378,66 @@ public function testAfterAfterSaveVisualSwatch($swatchType, $swatchValue) $this->eavAttribute->afterAfterSave($this->attribute); } + public function testDefaultTextualSwatchAfterSave() + { + $this->abstractSource->expects($this->once())->method('getAllOptions') + ->willReturn($this->allOptions); + + $this->swatch->expects($this->any())->method('getId') + ->willReturn(EavAttribute::DEFAULT_STORE_ID); + $this->swatch->expects($this->any())->method('save'); + $this->swatch->expects($this->any())->method('isDeleted') + ->with(false); + + $this->collection->expects($this->any())->method('addFieldToFilter') + ->willReturnSelf(); + $this->collection->expects($this->any())->method('getFirstItem') + ->willReturn($this->swatch); + $this->collectionFactory->expects($this->any())->method('create') + ->willReturn($this->collection); + + $this->attribute->expects($this->at(0))->method('getData') + ->willReturn($this->optionIds); + $this->attribute->expects($this->at(1))->method('getSource') + ->willReturn($this->abstractSource); + $this->attribute->expects($this->at(2))->method('getData') + ->with('default/0') + ->willReturn(null); + + $this->attribute->expects($this->at(3))->method('getData') + ->with('swatch/value') + ->willReturn( + [ + self::STORE_ID => [ + 1 => "test", + 2 => false, + 3 => null, + 4 => "", + ] + ] + ); + + $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') + ->with($this->attribute) + ->willReturn(true); + $this->swatchHelper->expects($this->once())->method('isVisualSwatch') + ->with($this->attribute) + ->willReturn(false); + $this->swatchHelper->expects($this->once())->method('isTextSwatch') + ->with($this->attribute) + ->willReturn(true); + + $this->swatch->expects($this->any())->method('setData') + ->withConsecutive( + ['option_id', self::OPTION_ID], + ['store_id', 1], + ['type', Swatch::SWATCH_TYPE_TEXTUAL], + ['value', "test"] + ); + + $this->eavAttribute->afterAfterSave($this->attribute); + } + public function testAfterAfterSaveTextualSwatch() { $this->abstractSource->expects($this->once())->method('getAllOptions') diff --git a/app/code/Magento/Swatches/i18n/en_US.csv b/app/code/Magento/Swatches/i18n/en_US.csv index 4ea18ba81355b..04101974cb4f5 100644 --- a/app/code/Magento/Swatches/i18n/en_US.csv +++ b/app/code/Magento/Swatches/i18n/en_US.csv @@ -34,3 +34,4 @@ Image,Image "Example format: 200x300.","Example format: 200x300." "Image Position","Image Position" "The value of Admin must be unique.","The value of Admin must be unique." +"Description","Description" diff --git a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml index 430d82f0a086a..d980133512072 100644 --- a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml +++ b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml @@ -21,16 +21,10 @@ $stores = $block->getStoresSortedBySortOrder(); escapeHtml(__('Is Default')) ?> - getId() != \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> - - escapeHtml(__('Swatch')); ?> - - - getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> - class="_required" - > - escapeHtml($_store->getName()) ?> + + escapeHtml($_store->getName()) ?> @@ -85,16 +79,15 @@ $stores = $block->getStoresSortedBySortOrder(); <%- data.checked %>getReadOnly()):?>disabled="disabled"/> - getId() != \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> + type="text" value="<%- data.swatchgetId() ?> %>" + placeholder="escapeHtml(__("Swatch")); ?>"/> - getStoresSortedBySortOrder(); type="text" getReadOnly() || $block->canManageOptionDefaultOnly()):?> disabled="disabled" - /> + + placeholder="escapeHtml(__("Description")); ?>"/> From 5a41bf506d0178144bacdf82cca58b70945a1dfe Mon Sep 17 00:00:00 2001 From: Teun Lassche Date: Wed, 31 May 2017 15:30:05 +0200 Subject: [PATCH 163/363] Fix issue #6999: Configurable attribute cache was never hit --- .../ConfigurableProduct/Model/Product/Type/Configurable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index e98f546943ff8..4e7b5fefa55bd 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -484,8 +484,8 @@ public function getConfigurableAttributes($product) */ protected function hasCacheData($configurableAttributes) { - $configurableAttributes = $configurableAttributes ?: unserialize($configurableAttributes); - if (is_array($configurableAttributes) && count($configurableAttributes)) { + $configurableAttributes = $configurableAttributes ? unserialize($configurableAttributes) : $configurableAttributes; + if ((is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable) && count($configurableAttributes)) { foreach ($configurableAttributes as $attribute) { /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $attribute */ if ($attribute->getData('options')) { From 42c71367ca5bcd73cd3c429eae065a67da905d44 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Wed, 31 May 2017 17:34:07 +0300 Subject: [PATCH 164/363] MAGETWO-60723: [Backport] - Nginx doesn't redirect to setup page when using port - for 2.1 --- lib/internal/Magento/Framework/App/Http.php | 3 +-- lib/internal/Magento/Framework/App/SetupInfo.php | 8 ++++++++ .../Magento/Framework/App/Test/Unit/SetupInfoTest.php | 7 +++++++ .../Magento/Framework/App/Test/Unit/_files/pub/index.php | 5 +++++ .../Framework/App/Test/Unit/_files/setup/index.php | 5 +++++ 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php create mode 100644 lib/internal/Magento/Framework/App/Test/Unit/_files/setup/index.php diff --git a/lib/internal/Magento/Framework/App/Http.php b/lib/internal/Magento/Framework/App/Http.php index d25116c8f79a0..10e93bbfba766 100644 --- a/lib/internal/Magento/Framework/App/Http.php +++ b/lib/internal/Magento/Framework/App/Http.php @@ -241,8 +241,7 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) . "because the Magento setup directory cannot be accessed. \n" . 'You can install Magento using either the command line or you must restore access ' . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; - $newMessage .= 'If you are using the sample nginx configuration, please go to ' - . $this->_request->getScheme(). '://' . $this->_request->getHttpHost() . $setupInfo->getUrl(); + throw new \Exception($newMessage, 0, $exception); } } diff --git a/lib/internal/Magento/Framework/App/SetupInfo.php b/lib/internal/Magento/Framework/App/SetupInfo.php index 4d845b53b5680..fac38e28ee2bc 100644 --- a/lib/internal/Magento/Framework/App/SetupInfo.php +++ b/lib/internal/Magento/Framework/App/SetupInfo.php @@ -147,6 +147,14 @@ public function isAvailable() { $setupDir = $this->getDir($this->projectRoot); $isSubDir = false !== strpos($setupDir . '/', $this->docRoot . '/'); + // Setup is not accessible from pub folder + $setupDir = rtrim($setupDir, '/'); + $lastOccurrence = strrpos($setupDir, '/pub/setup'); + + if (false !== $lastOccurrence) { + $setupDir = substr_replace($setupDir, '/setup', $lastOccurrence, strlen('/pub/setup')); + } + return $isSubDir && realpath($setupDir); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/SetupInfoTest.php b/lib/internal/Magento/Framework/App/Test/Unit/SetupInfoTest.php index 5f376ee776e00..37aca6fe76fe5 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/SetupInfoTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/SetupInfoTest.php @@ -193,6 +193,13 @@ public function isAvailableDataProvider() ], true ], + 'root within doc root + pub, existent sub-directory' => [ + [ + 'DOCUMENT_ROOT' => __DIR__ . '/_files/pub/', + 'SCRIPT_FILENAME' => __DIR__ . '/_files/pub/index.php', + ], + true + ], ]; } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php new file mode 100644 index 0000000000000..2a0cd37c68d37 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php @@ -0,0 +1,5 @@ + Date: Wed, 31 May 2017 17:44:50 +0300 Subject: [PATCH 165/363] MAGETWO-60723: [Backport] - Nginx doesn't redirect to setup page when using port - for 2.1 --- .../Magento/Framework/App/Test/Unit/_files/pub/index.php | 2 +- .../Magento/Framework/App/Test/Unit/_files/setup/index.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php index 2a0cd37c68d37..1e805048885be 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/index.php @@ -1,5 +1,5 @@ Date: Wed, 31 May 2017 18:58:16 +0300 Subject: [PATCH 166/363] MAGETWO-57291: [Backport] Logo Email for transactional emails can not be uploaded successfully - for 2.1 --- .../Model/Config/Backend/Email/Logo.php | 3 ++ .../Magento/Email/Model/AbstractTemplate.php | 2 +- .../Email/Model/Design/Backend/Logo.php | 36 +++++++++++++++++++ app/code/Magento/Email/composer.json | 1 + app/code/Magento/Email/etc/di.xml | 2 +- app/code/Magento/Email/etc/module.xml | 1 + .../Theme/Model/Design/Backend/Logo.php | 2 +- 7 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Email/Model/Design/Backend/Logo.php diff --git a/app/code/Magento/Config/Model/Config/Backend/Email/Logo.php b/app/code/Magento/Config/Model/Config/Backend/Email/Logo.php index b9fd2a859c56b..ab0dc47b3e233 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Email/Logo.php +++ b/app/code/Magento/Config/Model/Config/Backend/Email/Logo.php @@ -11,6 +11,9 @@ */ namespace Magento\Config\Model\Config\Backend\Email; +/** + * @deprecated + */ class Logo extends \Magento\Config\Model\Config\Backend\Image { /** diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 302d101bc7dc8..fbc7e8d9a60af 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -389,7 +389,7 @@ protected function getLogoUrl($store) $store ); if ($fileName) { - $uploadDir = \Magento\Config\Model\Config\Backend\Email\Logo::UPLOAD_DIR; + $uploadDir = \Magento\Email\Model\Design\Backend\Logo::UPLOAD_DIR; $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); if ($mediaDirectory->isFile($uploadDir . '/' . $fileName)) { return $this->storeManager->getStore()->getBaseUrl( diff --git a/app/code/Magento/Email/Model/Design/Backend/Logo.php b/app/code/Magento/Email/Model/Design/Backend/Logo.php new file mode 100644 index 0000000000000..d8e5e71dec494 --- /dev/null +++ b/app/code/Magento/Email/Model/Design/Backend/Logo.php @@ -0,0 +1,36 @@ + design/email/logo other_settings/email - Magento\Theme\Model\Design\Backend\Logo + Magento\Email\Model\Design\Backend\Logo media 1 diff --git a/app/code/Magento/Email/etc/module.xml b/app/code/Magento/Email/etc/module.xml index eb2b5cdf25a74..1e8317ca19e2e 100644 --- a/app/code/Magento/Email/etc/module.xml +++ b/app/code/Magento/Email/etc/module.xml @@ -10,6 +10,7 @@ + diff --git a/app/code/Magento/Theme/Model/Design/Backend/Logo.php b/app/code/Magento/Theme/Model/Design/Backend/Logo.php index 7d3f1bde1f23d..92f60a5f1d643 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/Logo.php +++ b/app/code/Magento/Theme/Model/Design/Backend/Logo.php @@ -21,7 +21,7 @@ class Logo extends Image */ protected function _getUploadDir() { - return $this->_mediaDirectory->getRelativePath($this->_appendScopeInfo(self::UPLOAD_DIR)); + return $this->_mediaDirectory->getRelativePath($this->_appendScopeInfo(static::UPLOAD_DIR)); } /** From 7e368365af588e898516a4698afc2c2719928316 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 11:14:38 +0300 Subject: [PATCH 167/363] MAGETWO-58571: [Backport] - [GITHUB] Custom option prices unexpectedly change after save #6116 - for 2.1 --- .../ResourceModel/Product/Option/Value.php | 25 +++- .../Magento/Catalog/Model/ProductTest.php | 19 ++- .../product_simple_with_custom_options.php | 113 ++++++++++++++++++ ...ct_simple_with_custom_options_rollback.php | 26 ++++ 4 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options_rollback.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 0acfed1411196..70ba35f75af98 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -33,6 +33,11 @@ class Value extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $_config; + /** + * @var \Magento\Framework\Locale\FormatInterface + */ + private $localeFormat; + /** * Class constructor * @@ -81,7 +86,7 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) } /** - * Save option value price data + * Save option value price data. * * @param \Magento\Framework\Model\AbstractModel $object * @return void @@ -91,8 +96,9 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $object) { $priceTable = $this->getTable('catalog_product_option_type_price'); + $formattedPrice = $this->getLocaleFormatter()->getNumber($object->getPrice()); - $price = (double)sprintf('%F', $object->getPrice()); + $price = (double)sprintf('%F', $formattedPrice); $priceType = $object->getPriceType(); if ($object->getPrice() && $priceType) { @@ -410,4 +416,19 @@ public function duplicate(\Magento\Catalog\Model\Product\Option\Value $object, $ return $object; } + + /** + * Get FormatInterface to convert price from string to number format. + * + * @return \Magento\Framework\Locale\FormatInterface + * @deprecated + */ + private function getLocaleFormatter() + { + if ($this->localeFormat === null) { + $this->localeFormat = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Locale\FormatInterface::class); + } + return $this->localeFormat; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index 148f769bb10e8..57989d80ad219 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -475,13 +475,24 @@ public function testValidateUniqueInputAttributeValue() } /** - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_options.php * @magentoAppIsolation enabled */ public function testGetOptions() { - $this->_model = $this->productRepository->get('simple'); - - $this->assertEquals(4, count($this->_model->getOptions())); + $this->_model = $this->productRepository->get('simple_with_custom_options'); + $options = $this->_model->getOptions(); + $this->assertNotEmpty($options); + $expectedValue = [ + '3-1-select' => 3000.00, + '3-2-select' => 5000.00, + '4-1-radio' => 600.234, + '4-2-radio' => 40000.00 + ]; + foreach ($options as $option) { + foreach ($option->getValues() as $value) { + $this->assertEquals($expectedValue[$value->getSku()], floatval($value->getPrice())); + } + } } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php new file mode 100644 index 0000000000000..fa6a8917fda35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php @@ -0,0 +1,113 @@ +reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple_with_custom_options') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->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' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +$oldOptions = [ + [ + 'previous_group' => 'select', + 'title' => 'Test Select', + 'type' => 'drop_down', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => '3,000.00', + 'price_type' => 'fixed', + 'sku' => '3-1-select', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => '5,000.00', + 'price_type' => 'fixed', + 'sku' => '3-2-select', + ], + ] + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Radio', + 'type' => 'radio', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => '600.234', + 'price_type' => 'fixed', + 'sku' => '4-1-radio', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => '40,000.00', + 'price_type' => 'fixed', + 'sku' => '4-2-radio', + ], + ] + ] +]; + +$options = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($oldOptions as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ + $option = $customOptionFactory->create(['data' => $option]); + $option->setProductSku($product->getSku()); + + $options[] = $option; +} + +$product->setOptions($options); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepositoryFactory = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepositoryFactory->save($product); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options_rollback.php new file mode 100644 index 0000000000000..dc35cf6b70ce8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options_rollback.php @@ -0,0 +1,26 @@ +getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple_with_custom_options', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 5f4cefe9397ac3303589f0ef986532109a665ef1 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 1 Jun 2017 13:09:58 +0300 Subject: [PATCH 168/363] MAGETWO-58855: [Backport] - Cart Price Rule (coupon) works no longer than current date - for 2.1 --- app/code/Magento/SalesRule/Model/CouponRepository.php | 11 ++++------- .../Test/Unit/Model/CouponRepositoryTest.php | 10 ++++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/SalesRule/Model/CouponRepository.php b/app/code/Magento/SalesRule/Model/CouponRepository.php index 8dca6a159d47c..2ee01b2489e6b 100644 --- a/app/code/Magento/SalesRule/Model/CouponRepository.php +++ b/app/code/Magento/SalesRule/Model/CouponRepository.php @@ -172,19 +172,16 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' ); } + $collection->setCurPage($searchCriteria->getCurrentPage()); $collection->setPageSize($searchCriteria->getPageSize()); - $coupons = []; - /** @var \Magento\SalesRule\Model\Coupon $couponModel */ - foreach ($collection->getItems() as $couponModel) { - $coupons[] = $couponModel->getData(); - } - + /** @var \Magento\SalesRule\Api\Data\CouponSearchResultInterface $searchResults */ $searchResults = $this->searchResultFactory->create(); $searchResults->setSearchCriteria($searchCriteria); - $searchResults->setItems($coupons); + $searchResults->setItems($collection->getItems()); $searchResults->setTotalCount($collection->getSize()); + return $searchResults; } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php index bba8a5355490b..d9697aa2ff9da 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php @@ -6,6 +6,7 @@ namespace Magento\SalesRule\Test\Unit\Model; use Magento\Framework\Api\SortOrder; +use Magento\SalesRule\Api\Data\CouponInterface; class CouponRepositoryTest extends \PHPUnit_Framework_TestCase { @@ -214,6 +215,11 @@ public function testGetList() $currentPage = 42; $pageSize = 4; + /** @var CouponInterface|\PHPUnit_Framework_MockObject_MockObject $couponMock */ + $couponMock = $this->getMockBuilder(CouponInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** * @var \Magento\Framework\Api\SearchCriteriaInterface $searchCriteriaMock */ @@ -248,8 +254,8 @@ public function testGetList() $collectionMock->expects($this->once())->method('setCurPage')->with($currentPage); $searchCriteriaMock->expects($this->once())->method('getPageSize')->willReturn($pageSize); $collectionMock->expects($this->once())->method('setPageSize')->with($pageSize); - $collectionMock->expects($this->once())->method('getItems')->willReturn([]); - $this->searchResultsMock->expects($this->once())->method('setItems')->with([]); + $collectionMock->expects($this->once())->method('getItems')->willReturn([$couponMock]); + $this->searchResultsMock->expects($this->once())->method('setItems')->with([$couponMock]); $this->searchResultFactory->expects($this->once())->method('create')->willReturn($this->searchResultsMock); $this->assertEquals($this->searchResultsMock, $this->model->getList($searchCriteriaMock)); From 8e17f18e707080593d67aa4c71b59e9f618588e8 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 13:36:10 +0300 Subject: [PATCH 169/363] MAGETWO-59173: [Backport] [Github] Disable email communication set to yes: email did get sent. #5988 - 2.1 --- .../Model/Mail/TransportInterfacePlugin.php | 51 ++++++++++++++++++ app/code/Magento/Email/Model/Template.php | 25 +++------ .../Email/Test/Unit/Model/TemplateTest.php | 20 +------ app/code/Magento/Email/etc/di.xml | 1 + .../Magento/Newsletter/Model/Template.php | 7 +-- .../Test/Unit/Model/TemplateTest.php | 52 +++++++++++++++++++ 6 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php diff --git a/app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php b/app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php new file mode 100644 index 0000000000000..7373e6e5589d1 --- /dev/null +++ b/app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php @@ -0,0 +1,51 @@ +scopeConfig = $scopeConfig; + } + + /** + * Omit email sending if disabled. + * + * @param TransportInterface $subject + * @param \Closure $proceed + * @return void + * @throws MailException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSendMessage( + TransportInterface $subject, + \Closure $proceed + ) { + if (!$this->scopeConfig->isSetFlag(self::XML_PATH_SYSTEM_SMTP_DISABLE, ScopeInterface::SCOPE_STORE)) { + $proceed(); + } + } +} diff --git a/app/code/Magento/Email/Model/Template.php b/app/code/Magento/Email/Model/Template.php index 268c58effb8ae..8b1b2adaa510a 100644 --- a/app/code/Magento/Email/Model/Template.php +++ b/app/code/Magento/Email/Model/Template.php @@ -5,26 +5,11 @@ */ namespace Magento\Email\Model; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Template model * - * Example: - * - * // Loading of template - * \Magento\Email\Model\TemplateFactory $templateFactory - * $templateFactory->create()->load($this->_scopeConfig->getValue( - * 'path_to_email_template_id_config', - * \Magento\Store\Model\ScopeInterface::SCOPE_STORE - * )); - * $variables = array( - * 'someObject' => $this->_coreResourceEmailTemplate - * 'someString' => 'Some string value' - * ); - * $emailTemplate->send('some@domain.com', 'Name Of User', $variables); - * * @method \Magento\Email\Model\ResourceModel\Template _getResource() * @method \Magento\Email\Model\ResourceModel\Template getResource() * @method string getTemplateCode() @@ -62,7 +47,10 @@ class Template extends AbstractTemplate implements \Magento\Framework\Mail\Templ const XML_PATH_SENDING_RETURN_PATH_EMAIL = 'system/smtp/return_path_email'; /** - * Config path to mail sending setting that shows if email communications are disabled + * Config path to mail sending setting that shows if email communications are disabled. + * + * @deprecated + * @see \Magento\Email\Model\Mail\TransportInterfacePlugin::XML_PATH_SYSTEM_SMTP_DISABLE */ const XML_PATH_SYSTEM_SMTP_DISABLE = 'system/smtp/disable'; @@ -190,14 +178,13 @@ public function setId($value) } /** - * Return true if this template can be used for sending queue as main template + * Return true if this template can be used for sending queue as main template. * * @return bool */ public function isValidForSend() { - return !$this->scopeConfig->isSetFlag(Template::XML_PATH_SYSTEM_SMTP_DISABLE, ScopeInterface::SCOPE_STORE) - && $this->getSenderName() && $this->getSenderEmail() && $this->getTemplateSubject(); + return $this->getSenderName() && $this->getSenderEmail() && $this->getTemplateSubject(); } /** diff --git a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php index 189734260d0cb..240e147f2da3a 100644 --- a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php @@ -5,7 +5,6 @@ */ namespace Magento\Email\Test\Unit\Model; -use Magento\Email\Model\Template\Filter; use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\TemplateTypesInterface; @@ -426,18 +425,14 @@ public function testGetAndSetId() } /** - * @param $isSMTPDisabled bool * @param $senderName string * @param $senderEmail string * @param $templateSubject string + * @param $expectedValue * @dataProvider isValidForSendDataProvider */ - public function testIsValidForSend($isSMTPDisabled, $senderName, $senderEmail, $templateSubject, $expectedValue) + public function testIsValidForSend($senderName, $senderEmail, $templateSubject, $expectedValue) { - $this->scopeConfig->expects($this->once()) - ->method('isSetFlag') - ->with('system/smtp/disable', ScopeInterface::SCOPE_STORE) - ->will($this->returnValue($isSMTPDisabled)); $model = $this->getModelMock(['getSenderName', 'getSenderEmail', 'getTemplateSubject']); $model->expects($this->any()) ->method('getSenderName') @@ -455,35 +450,24 @@ public function isValidForSendDataProvider() { return [ 'should be valid' => [ - 'isSMTPDisabled' => false, 'senderName' => 'sender name', 'senderEmail' => 'email@example.com', 'templateSubject' => 'template subject', 'expectedValue' => true ], - 'no smtp so not valid' => [ - 'isSMTPDisabled' => true, - 'senderName' => 'sender name', - 'senderEmail' => 'email@example.com', - 'templateSubject' => 'template subject', - 'expectedValue' => false - ], 'no sender name so not valid' => [ - 'isSMTPDisabled' => false, 'senderName' => '', 'senderEmail' => 'email@example.com', 'templateSubject' => 'template subject', 'expectedValue' => false ], 'no sender email so not valid' => [ - 'isSMTPDisabled' => false, 'senderName' => 'sender name', 'senderEmail' => '', 'templateSubject' => 'template subject', 'expectedValue' => false ], 'no subject so not valid' => [ - 'isSMTPDisabled' => false, 'senderName' => 'sender name', 'senderEmail' => 'email@example.com', 'templateSubject' => '', diff --git a/app/code/Magento/Email/etc/di.xml b/app/code/Magento/Email/etc/di.xml index 4b9f9b259d853..954b071e81db6 100644 --- a/app/code/Magento/Email/etc/di.xml +++ b/app/code/Magento/Email/etc/di.xml @@ -59,5 +59,6 @@ + diff --git a/app/code/Magento/Newsletter/Model/Template.php b/app/code/Magento/Newsletter/Model/Template.php index 694097700cdd7..e8f1fabd04683 100644 --- a/app/code/Magento/Newsletter/Model/Template.php +++ b/app/code/Magento/Newsletter/Model/Template.php @@ -232,15 +232,12 @@ protected function getFilterFactory() } /** - * Check if template can be added to newsletter queue + * Check if template can be added to newsletter queue. * * @return boolean */ public function isValidForSend() { - return !$this->scopeConfig->isSetFlag( - \Magento\Email\Model\Template::XML_PATH_SYSTEM_SMTP_DISABLE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) && $this->getTemplateSenderName() && $this->getTemplateSenderEmail() && $this->getTemplateSubject(); + return $this->getTemplateSenderName() && $this->getTemplateSenderEmail() && $this->getTemplateSubject(); } } diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php index 30c94d28ffd91..2806d9219bb64 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php @@ -379,4 +379,56 @@ public function getProcessedTemplateDataProvider() ], ]; } + + /** + * @param $senderName string + * @param $senderEmail string + * @param $templateSubject string + * @param $expectedValue + * @dataProvider isValidForSendDataProvider + */ + public function testIsValidForSend($senderName, $senderEmail, $templateSubject, $expectedValue) + { + $model = $this->getModelMock(['getTemplateSenderName', 'getTemplateSenderEmail', 'getTemplateSubject']); + $model->expects($this->any()) + ->method('getTemplateSenderName') + ->will($this->returnValue($senderName)); + $model->expects($this->any()) + ->method('getTemplateSenderEmail') + ->will($this->returnValue($senderEmail)); + $model->expects($this->any()) + ->method('getTemplateSubject') + ->will($this->returnValue($templateSubject)); + $this->assertEquals($expectedValue, $model->isValidForSend()); + } + + public function isValidForSendDataProvider() + { + return [ + 'should be valid' => [ + 'senderName' => 'sender name', + 'senderEmail' => 'email@example.com', + 'templateSubject' => 'template subject', + 'expectedValue' => true + ], + 'no sender name so not valid' => [ + 'senderName' => '', + 'senderEmail' => 'email@example.com', + 'templateSubject' => 'template subject', + 'expectedValue' => false + ], + 'no sender email so not valid' => [ + 'senderName' => 'sender name', + 'senderEmail' => '', + 'templateSubject' => 'template subject', + 'expectedValue' => false + ], + 'no subject so not valid' => [ + 'senderName' => 'sender name', + 'senderEmail' => 'email@example.com', + 'templateSubject' => '', + 'expectedValue' => false + ], + ]; + } } From b4704f5fd7258c4cc0f12e295ca513fe37fae79b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 1 Jun 2017 13:44:10 +0300 Subject: [PATCH 170/363] MAGETWO-56988: [Backport] - [GitHub] No Swatch Input for Admin Scope. No Fallback to Admin Scope #5143 #5142 - for 2.1 --- .../Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php index b9f69bb0ec332..e1566d9ee2bc2 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php @@ -378,6 +378,11 @@ public function testAfterAfterSaveVisualSwatch($swatchType, $swatchValue) $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Tests afterAfterSave() for test swatch attribute. + * + * @return void + */ public function testDefaultTextualSwatchAfterSave() { $this->abstractSource->expects($this->once())->method('getAllOptions') From c108f3aa05d4c173ba452537d7103123ff56d2c2 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 13:56:13 +0300 Subject: [PATCH 171/363] MAGETWO-58571: [Backport] - [GITHUB] Custom option prices unexpectedly change after save #6116 - for 2.1 --- .../Magento/Catalog/Model/ResourceModel/Product/Option/Value.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 70ba35f75af98..a3df7bbc083fd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -429,6 +429,7 @@ private function getLocaleFormatter() $this->localeFormat = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Locale\FormatInterface::class); } + return $this->localeFormat; } } From f74cdba0693d2661974035f44cea6d51bab22107 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 14:26:22 +0300 Subject: [PATCH 172/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php | 2 +- .../Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index e64a4d3e26c0a..1fbbcb23cc43c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -70,7 +70,7 @@ public function testModifyMeta() $this->prepareDataForComparison($actualMeta, $expectedMeta); $this->assertEquals($expectedMeta, $actualMeta); } - /* + /** * Test modifying meta on new product. */ public function testModifyMetaNewProduct() diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php index 55814b92fab7f..f34c7caa1e045 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/ResourceModel/UpdateHandlerTest.php @@ -8,6 +8,8 @@ use Magento\TestFramework\Helper\Bootstrap; /** + * Class for testing update handler. + * * @magentoAppArea adminhtml * @magentoAppIsolation enabled */ @@ -223,6 +225,8 @@ public function getCustomStoreDataProvider() } /** + * Get custom attribute data provider. + * * @return array */ public function getCustomAttributeDataProvider() From bf6155afa596091c6573451b5b043bb5969925ae Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 14:32:46 +0300 Subject: [PATCH 173/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 388672daf1e99..cfe6598540154 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -520,6 +520,8 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi } /** + * Setup attribute meta data provider. + * * @return array */ public function setupAttributeMetaDataProvider() From 4e5dceaa03d56b488d8f34e2e67aa804f91f5df4 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 1 Jun 2017 14:41:25 +0300 Subject: [PATCH 174/363] MAGETWO-59173: [Backport] [Github] Disable email communication set to yes: email did get sent. #5988 - 2.1 --- app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php index 2806d9219bb64..7381da3a716a7 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php @@ -336,6 +336,8 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e } /** + * Get processed template data provider. + * * @return array */ public function getProcessedTemplateDataProvider() From b4a7620c216b12ec7861381ac7a5246c541e79be Mon Sep 17 00:00:00 2001 From: Teun Lassche Date: Thu, 1 Jun 2017 15:32:10 +0200 Subject: [PATCH 175/363] Fix issue #6999: Configurable attribute cache was never hit, code style --- .../Model/Product/Type/Configurable.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 4e7b5fefa55bd..1ca0eac1773d5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock\Status; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; @@ -18,7 +19,6 @@ use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; /** * Configurable product type implementation @@ -484,8 +484,9 @@ public function getConfigurableAttributes($product) */ protected function hasCacheData($configurableAttributes) { - $configurableAttributes = $configurableAttributes ? unserialize($configurableAttributes) : $configurableAttributes; - if ((is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable) && count($configurableAttributes)) { + $configurableAttributes = $configurableAttributes ? unserialize( $configurableAttributes ) : $configurableAttributes; + $isTraversable = (is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable); + if ($isTraversable && count($configurableAttributes)) { foreach ($configurableAttributes as $attribute) { /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $attribute */ if ($attribute->getData('options')) { From 1222362f876afbc3f1dfa81d67e7fa954e9155aa Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Thu, 1 Jun 2017 17:03:42 +0300 Subject: [PATCH 176/363] MAGETWO-62995: Product imports not Auto-Generating URL Keys for SKUs - for 2.1.x --- .../Model/Import/Product.php | 23 ++++++++--- .../Model/Import/ProductTest.php | 41 +++++++++++++++++++ .../products_to_import_without_url_keys.csv | 4 ++ 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index ac5fe77c2498f..6be6df1740c94 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -568,7 +568,10 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** @var array */ protected $productUrlSuffix = []; - /** @var array */ + /** + * @var array + * @deprecated + */ protected $productUrlKeys = []; /** @@ -1521,6 +1524,10 @@ protected function _saveProducts() } $rowScope = $this->getRowScope($rowData); + if (empty($rowData[self::URL_KEY])) { + $rowData[self::URL_KEY] = $this->getUrlKey($rowData); + } + $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { @@ -2636,18 +2643,22 @@ protected function getProductUrlSuffix($storeId = null) } /** + * Return url key if specified, or generate one from product name otherwise. + * * @param array $rowData * @return string */ protected function getUrlKey($rowData) { if (!empty($rowData[self::URL_KEY])) { - $this->productUrlKeys[$rowData[self::COL_SKU]] = $rowData[self::URL_KEY]; + return $rowData[self::URL_KEY]; } - $urlKey = !empty($this->productUrlKeys[$rowData[self::COL_SKU]]) - ? $this->productUrlKeys[$rowData[self::COL_SKU]] - : $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); - return $urlKey; + + if (!empty($rowData[self::COL_NAME])) { + return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); + } + + return ''; } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 95c3939aa52b4..b6ffc358aa0f5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1142,6 +1142,47 @@ public function testValidateUrlKeys($importFile, $errorsCount) } } + /** + * Test import products without url keys will auto generate ones. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoAppIsolation enabled + */ + public function testImportWithoutUrlKeys() + { + $products = [ + 'simple1' => 'simple-1', + 'simple2' => 'simple-2', + 'simple3' => 'simple-3' + ]; + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_without_url_keys.csv', + 'directory' => $directory + ] + ); + + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + ) + ->setSource($source) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + foreach ($products as $productSku => $productUrlKey) { + $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); + } + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv new file mode 100644 index 0000000000000..c7efa4cb4bc30 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv @@ -0,0 +1,4 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,url_key +simple1,simple,,"simple 1",25,Default,"" +simple2,simple,,"simple 2",34,Default,"" +simple3,simple,,"simple 3",58,Default,"" \ No newline at end of file From 369190eca251d7fc1a5c11d2c4d68f41f7f5e576 Mon Sep 17 00:00:00 2001 From: Teun Lassche Date: Thu, 1 Jun 2017 16:19:19 +0200 Subject: [PATCH 177/363] Fix issue #6999: Configurable attribute cache was never hit, code style --- .../ConfigurableProduct/Model/Product/Type/Configurable.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 1ca0eac1773d5..a7091e4c09e31 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -484,7 +484,10 @@ public function getConfigurableAttributes($product) */ protected function hasCacheData($configurableAttributes) { - $configurableAttributes = $configurableAttributes ? unserialize( $configurableAttributes ) : $configurableAttributes; + if ($configurableAttributes) + { + $configurableAttributes = unserialize($configurableAttributes); + } $isTraversable = (is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable); if ($isTraversable && count($configurableAttributes)) { foreach ($configurableAttributes as $attribute) { From b7486e6fac414a5d1f762c1e75d65eceffa9e5ef Mon Sep 17 00:00:00 2001 From: Teun Lassche Date: Thu, 1 Jun 2017 16:37:29 +0200 Subject: [PATCH 178/363] Fix issue #6999: Configurable attribute cache was never hit, code style --- .../ConfigurableProduct/Model/Product/Type/Configurable.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index a7091e4c09e31..803517a5c7909 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -484,8 +484,7 @@ public function getConfigurableAttributes($product) */ protected function hasCacheData($configurableAttributes) { - if ($configurableAttributes) - { + if ($configurableAttributes) { $configurableAttributes = unserialize($configurableAttributes); } $isTraversable = (is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable); From 88ed064a6c08061dd9d8de508a47b8ffc1a754ff Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi Date: Thu, 1 Jun 2017 18:42:06 +0300 Subject: [PATCH 179/363] MAGETWO-64095: Prepare code base 2.1.8 --- composer.json | 2 +- composer.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 683565c98917b..3dd794d8d08be 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento2ce", "description": "Magento 2 (Community Edition)", "type": "project", - "version": "2.1.7", + "version": "2.1.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/composer.lock b/composer.lock index 266d8e166c075..6295c288e3973 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "be17b1a81096da0ad3940a692025ca5c", - "content-hash": "a10bae9589fd59180a22f9b14d416c0c", + "hash": "f1f9aac0b690450209e74ad01999f2e3", + "content-hash": "1786257abb2e516048356079de807846", "packages": [ { "name": "braintree/braintree_php", From 96e8eb77b3f50d4f4f598f2003bdcaa589aede1f Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 2 Jun 2017 09:14:32 +0300 Subject: [PATCH 180/363] MAGETWO-59173: [Backport] [Github] Disable email communication set to yes: email did get sent. #5988 - 2.1 --- .../Email/Model/{Mail => Plugin}/TransportInterfacePlugin.php | 4 +++- app/code/Magento/Email/etc/di.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) rename app/code/Magento/Email/Model/{Mail => Plugin}/TransportInterfacePlugin.php (94%) diff --git a/app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php similarity index 94% rename from app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php rename to app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php index 7373e6e5589d1..877a645472e2e 100644 --- a/app/code/Magento/Email/Model/Mail/TransportInterfacePlugin.php +++ b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php @@ -3,7 +3,7 @@ * Copyright © 2013-2017 Magento. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Email\Model\Mail; +namespace Magento\Email\Model\Plugin; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\MailException; @@ -18,6 +18,8 @@ class TransportInterfacePlugin const XML_PATH_SYSTEM_SMTP_DISABLE = 'system/smtp/disable'; /** + * Application config. + * * @var ScopeConfigInterface */ private $scopeConfig; diff --git a/app/code/Magento/Email/etc/di.xml b/app/code/Magento/Email/etc/di.xml index 954b071e81db6..8b11fd396610b 100644 --- a/app/code/Magento/Email/etc/di.xml +++ b/app/code/Magento/Email/etc/di.xml @@ -59,6 +59,6 @@ - + From c7dd3c306d50d7bd09310251a80337edb026c5ab Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Fri, 2 Jun 2017 10:42:48 +0300 Subject: [PATCH 181/363] MAGETWO-59173: [Backport] [Github] Disable email communication set to yes: email did get sent. #5988 - 2.1 --- .../Magento/Email/Model/Plugin/TransportInterfacePlugin.php | 6 +++++- app/code/Magento/Email/Model/Template.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php index 877a645472e2e..91056e3e7dc38 100644 --- a/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php +++ b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php @@ -1,8 +1,9 @@ Date: Fri, 2 Jun 2017 11:05:11 +0300 Subject: [PATCH 182/363] MAGETWO-59173: [Backport] [Github] Disable email communication set to yes: email did get sent. #5988 - 2.1 --- .../Magento/Email/Model/Plugin/TransportInterfacePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php index 91056e3e7dc38..066ef61d9714e 100644 --- a/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php +++ b/app/code/Magento/Email/Model/Plugin/TransportInterfacePlugin.php @@ -12,7 +12,7 @@ use Magento\Store\Model\ScopeInterface; /** - * Class TransportInterfacePlugin. + * Plugin allow omit email sending if option is disabled. */ class TransportInterfacePlugin { From beae948d75a5bb8180d5167d432d0cca48bef4b2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 2 Jun 2017 13:04:28 +0300 Subject: [PATCH 183/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Product/Initialization/Helper.php | 65 ++++++-- .../Model/Product/Option/Repository.php | 48 +++++- .../Model/Product/Option/SaveHandler.php | 24 ++- .../Model/Product/Option/Validator/Select.php | 5 + .../Catalog/Model/Product/Option/Value.php | 2 +- .../Model/ResourceModel/Product/Option.php | 47 +++--- .../ResourceModel/Product/Option/Value.php | 7 + .../Product/Initialization/HelperTest.php | 150 +++++++++++++----- .../Model/Product/Option/RepositoryTest.php | 122 ++++++++++++-- .../Model/Product/Option/SaveHandlerTest.php | 75 +++++++++ .../Form/Modifier/CustomOptionsTest.php | 44 +++-- .../Product/Form/Modifier/CustomOptions.php | 41 ++++- .../helper/custom-option-type-service.html | 17 ++ .../Product/Edit/Section/Options.php | 28 +++- .../Edit/Section/Options/Type/DropDown.php | 16 +- .../Edit/Section/Options/Type/DropDown.xml | 5 + .../Block/Adminhtml/Product/ProductForm.xml | 5 + ...ssertProductCustomOptionsOnProductPage.php | 12 +- .../Test/Repository/Product/CustomOptions.xml | 16 ++ .../Product/UpdateSimpleProductEntityTest.xml | 8 + .../Magento/Catalog/_files/product_simple.php | 16 +- .../product_simple_with_admin_store.php | 18 +-- .../_files/product_export_data.php | 85 +++++----- .../Product/Type/Configurable/PriceTest.php | 3 +- .../_files/product_simple_77.php | 8 +- 25 files changed, 686 insertions(+), 181 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/template/form/element/helper/custom-option-type-service.html diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 52addeb6d6280..e31f1adc116d7 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -199,14 +199,18 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra $customOptions = []; foreach ($options as $customOptionData) { if (empty($customOptionData['is_delete'])) { + if (empty($customOptionData['option_id'])) { + $customOptionData['option_id'] = null; + } + if (isset($customOptionData['values'])) { $customOptionData['values'] = array_filter($customOptionData['values'], function ($valueData) { return empty($valueData['is_delete']); }); } + $customOption = $this->getCustomOptionFactory()->create(['data' => $customOptionData]); $customOption->setProductSku($product->getSku()); - $customOption->setOptionId(null); $customOptions[] = $customOption; } } @@ -315,21 +319,59 @@ public function mergeProductOptions($productOptions, $overwriteOptions) return $productOptions; } - foreach ($productOptions as $index => $option) { + foreach ($productOptions as $optionIndex => $option) { $optionId = $option['option_id']; + $option = $this->overwriteValue( + $optionId, + $option, + $overwriteOptions + ); - if (!isset($overwriteOptions[$optionId])) { - continue; + if (isset($option['values']) && isset($overwriteOptions[$optionId]['values'])) { + foreach ($option['values'] as $valueIndex => $value) { + if (isset($value['option_type_id'])) { + $valueId = $value['option_type_id']; + $value = $this->overwriteValue( + $valueId, + $value, + $overwriteOptions[$optionId]['values'] + ); + + $option['values'][$valueIndex] = $value; + } + } } + $productOptions[$optionIndex] = $option; + } + + return $productOptions; + } + + /** + * Overwrite values of fields to default, if there are option id and field + * name in array overwriteOptions. + * + * @param int $optionId + * @param array $option + * @param array $overwriteOptions + * + * @return array + */ + private function overwriteValue($optionId, $option, $overwriteOptions) + { + if (isset($overwriteOptions[$optionId])) { foreach ($overwriteOptions[$optionId] as $fieldName => $overwrite) { if ($overwrite && isset($option[$fieldName]) && isset($option['default_' . $fieldName])) { - $productOptions[$index][$fieldName] = $option['default_' . $fieldName]; + $option[$fieldName] = $option['default_' . $fieldName]; + if ('title' == $fieldName) { + $option['is_delete_store_title'] = 1; + } } } } - return $productOptions; + return $option; } /** @@ -339,8 +381,9 @@ private function getCustomOptionFactory() { if (null === $this->customOptionFactory) { $this->customOptionFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory'); + ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); } + return $this->customOptionFactory; } @@ -351,8 +394,9 @@ private function getProductLinkFactory() { if (null === $this->productLinkFactory) { $this->productLinkFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Api\Data\ProductLinkInterfaceFactory'); + ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); } + return $this->productLinkFactory; } @@ -363,8 +407,9 @@ private function getProductRepository() { if (null === $this->productRepository) { $this->productRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Api\ProductRepositoryInterface\Proxy'); + ->get(\Magento\Catalog\Api\ProductRepositoryInterface\Proxy::class); } + return $this->productRepository; } @@ -377,6 +422,7 @@ private function getLinkResolver() if (!is_object($this->linkResolver)) { $this->linkResolver = ObjectManager::getInstance()->get(LinkResolver::class); } + return $this->linkResolver; } @@ -391,6 +437,7 @@ private function getDateTimeFilter() $this->dateTimeFilter = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); } + return $this->dateTimeFilter; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php index 6f61de9809a8b..5bd162c1ffacb 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php @@ -133,15 +133,53 @@ public function duplicate( */ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option) { + /** @var string $productSku */ $productSku = $option->getProductSku(); + if (!$productSku) { throw new CouldNotSaveException(__('ProductSku should be specified')); } + + /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->get($productSku); + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); $option->setData('product_id', $product->getData($metadata->getLinkField())); - $option->setOptionId(null); + $option->setData('store_id', $product->getStoreId()); + + if ($option->getOptionId()) { + + $options = $product->getOptions(); + + if (!$options) { + $options = $this->getProductOptions($product); + } + + $persistedOption = array_filter( + $options, + function ($iOption) use ($option) { + return $option->getOptionId() == $iOption->getOptionId(); + } + ); + $persistedOption = reset($persistedOption); + + if (!$persistedOption) { + throw new NoSuchEntityException(); + } + + /** @var array $originalValues */ + $originalValues = $persistedOption->getValues(); + /** @var array $newValues */ + $newValues = $option->getData('values'); + + if ($newValues) { + $newValues = $this->markRemovedValues($newValues, $originalValues); + $option->setData('values', $newValues); + } + } + $option->save(); + return $option; } @@ -203,7 +241,7 @@ private function getOptionFactory() { if (null === $this->optionFactory) { $this->optionFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\Product\OptionFactory'); + ->get(\Magento\Catalog\Model\Product\OptionFactory::class); } return $this->optionFactory; } @@ -216,7 +254,7 @@ private function getCollectionFactory() { if (null === $this->collectionFactory) { $this->collectionFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory'); + ->get(\Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory::class); } return $this->collectionFactory; } @@ -229,7 +267,7 @@ private function getMetadataPool() { if (null === $this->metadataPool) { $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\MetadataPool'); + ->get(\Magento\Framework\EntityManager\MetadataPool::class); } return $this->metadataPool; } @@ -242,7 +280,7 @@ private function getHydratorPool() { if (null === $this->hydratorPool) { $this->hydratorPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\HydratorPool'); + ->get(\Magento\Framework\EntityManager\HydratorPool::class); } return $this->hydratorPool; } diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index cb089a95a4b97..0f77d9e26ceb0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -20,7 +20,6 @@ class SaveHandler implements ExtensionInterface /** * @param OptionRepository $optionRepository - * @param MetadataPool $metadataPool */ public function __construct( OptionRepository $optionRepository @@ -36,15 +35,32 @@ public function __construct( */ public function execute($entity, $arguments = []) { + $options = $entity->getOptions(); + $optionIds = []; + + if ($options) { + $optionIds = array_map( + function ($option) { + /** @var \Magento\Catalog\Model\Product\Option $option */ + return $option->getOptionId(); + }, + $options + ); + } + /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ foreach ($this->optionRepository->getProductOptions($entity) as $option) { - $this->optionRepository->delete($option); + if (!in_array($option->getOptionId(), $optionIds)) { + $this->optionRepository->delete($option); + } } - if ($entity->getOptions()) { - foreach ($entity->getOptions() as $option) { + + if ($options) { + foreach ($options as $option) { $this->optionRepository->save($option); } } + return $entity; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php index 8dc506036ad43..80e24fba67d16 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php @@ -51,6 +51,11 @@ protected function validateOptionValue(Option $option) $storeId = $option->getProduct()->getStoreId(); } foreach ($values as $value) { + + if (isset($value['is_delete']) && (bool)$value['is_delete']) { + continue; + } + $type = isset($value['price_type']) ? $value['price_type'] : null; $price = isset($value['price']) ? $value['price'] : null; $title = isset($value['title']) ? $value['title'] : null; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 58566977a15c9..c0b86bd2bb0d1 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -200,7 +200,7 @@ public function saveValues() 'store_id', $this->getOption()->getStoreId() ); - $this->unsetData('option_type_id'); + if ($this->getData('is_delete') == '1') { if ($this->getId()) { $this->deleteValues($this->getId()); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php index 2fcfae56a434f..9f8ace3d445d4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Model\Store; /** * Catalog product custom option resource model @@ -122,7 +123,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje $object->getId() )->where( 'store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ); $optionId = $connection->fetchOne($statement); @@ -139,7 +140,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje $data, [ 'option_id = ?' => $object->getId(), - 'store_id = ?' => \Magento\Store\Model\Store::DEFAULT_STORE_ID + 'store_id = ?' => Store::DEFAULT_STORE_ID ] ); } else { @@ -147,7 +148,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje new \Magento\Framework\DataObject( [ 'option_id' => $object->getId(), - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + 'store_id' => Store::DEFAULT_STORE_ID, 'price' => $object->getPrice(), 'price_type' => $object->getPriceType(), ] @@ -159,11 +160,11 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje } $scope = (int)$this->_config->getValue( - \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, + Store::XML_PATH_PRICE_SCOPE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - if ($object->getStoreId() != '0' && $scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) { + if ($object->getStoreId() != '0' && $scope == Store::PRICE_SCOPE_WEBSITE) { $baseCurrency = $this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, 'default' @@ -222,7 +223,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje } } } - } elseif ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price') + } elseif ($scope == Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price') ) { $connection->delete( $priceTable, @@ -245,16 +246,19 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje { $connection = $this->getConnection(); $titleTableName = $this->getTable('catalog_product_option_title'); - foreach ([\Magento\Store\Model\Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { + foreach ([Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { $existInCurrentStore = $this->getColFromOptionTable($titleTableName, (int)$object->getId(), (int)$storeId); - $existInDefaultStore = $this->getColFromOptionTable( - $titleTableName, - (int)$object->getId(), - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); + $existInDefaultStore = (int)$storeId == Store::DEFAULT_STORE_ID ? + $existInCurrentStore : + $this->getColFromOptionTable($titleTableName, (int)$object->getId(), Store::DEFAULT_STORE_ID); + if ($object->getTitle()) { + $isDeleteStoreTitle = (bool)$object->getData('is_delete_store_title'); if ($existInCurrentStore) { - if ($object->getStoreId() == $storeId) { + if ($isDeleteStoreTitle && (int)$storeId != Store::DEFAULT_STORE_ID) { + $connection->delete($titleTableName, ['option_title_id = ?' => $existInCurrentStore]); + + } elseif ($object->getStoreId() == $storeId) { $data = $this->_prepareDataForTable( new \Magento\Framework\DataObject(['title' => $object->getTitle()]), $titleTableName @@ -270,8 +274,12 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje } } else { // we should insert record into not default store only of if it does not exist in default store - if (($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore) - || ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInCurrentStore) + if (($storeId == Store::DEFAULT_STORE_ID && !$existInDefaultStore) || + ( + $storeId != Store::DEFAULT_STORE_ID && + !$existInCurrentStore && + !$isDeleteStoreTitle + ) ) { $data = $this->_prepareDataForTable( new \Magento\Framework\DataObject( @@ -287,7 +295,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje } } } else { - if ($object->getId() && $object->getStoreId() > \Magento\Store\Model\Store::DEFAULT_STORE_ID + if ($object->getId() && $object->getStoreId() > Store::DEFAULT_STORE_ID && $storeId ) { $connection->delete( @@ -466,7 +474,7 @@ public function getSearchableData($productId, $storeId) 'option_title_default.option_id=product_option.option_id', $connection->quoteInto( 'option_title_default.store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ) ] ); @@ -513,7 +521,7 @@ public function getSearchableData($productId, $storeId) 'option_title_default.option_type_id=option_type.option_type_id', $connection->quoteInto( 'option_title_default.store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ) ] ); @@ -567,8 +575,9 @@ private function getMetadataPool() { if (null === $this->metadataPool) { $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\MetadataPool'); + ->get(\Magento\Framework\EntityManager\MetadataPool::class); } + return $this->metadataPool; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 0acfed1411196..32c44fa43bf99 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -231,6 +231,13 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje ); $optionTypeId = $this->getConnection()->fetchOne($select); $existInCurrentStore = $this->getOptionIdFromOptionTable($titleTable, (int)$object->getId(), (int)$storeId); + + if ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && + $object->getData('is_delete_store_title') + ) { + $object->unsetData('title'); + } + if ($object->getTitle()) { if ($existInCurrentStore) { if ($storeId == $object->getStoreId()) { diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index a3a08ceb77fd0..fa60ea8ee6fd6 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -6,10 +6,11 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; -use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; @@ -17,7 +18,6 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\DateTime\Filter\Date as DateFilter; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; -use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; /** @@ -95,7 +95,7 @@ class HelperTest extends \PHPUnit_Framework_TestCase protected $customOptionFactoryMock; /** - * @var ProductCustomOptionInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Option|\PHPUnit_Framework_MockObject_MockObject */ protected $customOptionMock; @@ -136,8 +136,6 @@ protected function setUp() ->getMock(); $this->productMock = $this->getMockBuilder(Product::class) ->setMethods([ - 'setData', - 'addData', 'getId', 'setWebsiteIds', 'isLockedAttribute', @@ -145,7 +143,6 @@ protected function setUp() 'getAttributes', 'unlockAttribute', 'getOptionsReadOnly', - 'setOptions', 'setCanSaveCustomOptions', '__sleep', '__wakeup', @@ -159,9 +156,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->customOptionMock = $this->getMockBuilder(ProductCustomOptionInterface::class) + $this->customOptionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->setMethods(null) + ->getMock(); $this->productLinksMock = $this->getMockBuilder(ProductLinks::class) ->disableOriginalConstructor() ->getMock(); @@ -196,14 +194,10 @@ protected function setUp() */ public function testInitialize() { - $this->customOptionMock->expects($this->once()) - ->method('setProductSku'); - $this->customOptionMock->expects($this->once()) - ->method('setOptionId'); - $optionsData = [ - 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1'], - 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1'], + 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''], + 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'], + 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'] ]; $productData = [ 'stock_data' => ['stock_data'], @@ -277,35 +271,44 @@ public function testInitialize() ->method('getAttributes') ->willReturn($attributesArray); - $productData['category_ids'] = []; - $productData['website_ids'] = []; - unset($productData['options']); - - $this->productMock->expects($this->once()) - ->method('addData') - ->with($productData); - $this->productMock->expects($this->once()) + $this->productMock->expects($this->any()) ->method('getSku') ->willReturn('sku'); $this->productMock->expects($this->any()) ->method('getOptionsReadOnly') ->willReturn(false); + $customOptionMockClone1 = clone $this->customOptionMock; + $customOptionMockClone2 = clone $this->customOptionMock; + $this->customOptionFactoryMock->expects($this->any()) ->method('create') - ->with(['data' => $optionsData['option2']]) - ->willReturn($this->customOptionMock); - $this->productMock->expects($this->once()) - ->method('setOptions') - ->with([$this->customOptionMock]); + ->willReturnMap([ + [ + ['data' => $optionsData['option2']], + $customOptionMockClone1->setData($optionsData['option2']) + ], [ + ['data' => $optionsData['option3']], + $customOptionMockClone2->setData($optionsData['option3']) + ] + ]); $this->assertEquals($this->productMock, $this->helper->initialize($this->productMock)); + + $productOptions = $this->productMock->getOptions(); + $this->assertTrue(2 == count($productOptions)); + list($option2, $option3) = $productOptions; + $this->assertTrue($option2->getOptionId() == $optionsData['option2']['option_id']); + $this->assertTrue('sku' == $option2->getData('product_sku')); + $this->assertTrue($option3->getOptionId() == $optionsData['option3']['option_id']); + $this->assertTrue('sku' == $option2->getData('product_sku')); } /** * Data provider for testMergeProductOptions * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function mergeProductOptionsDataProvider() { @@ -325,15 +328,34 @@ public function mergeProductOptionsDataProvider() [ 'option_id' => '3', 'key1' => 'val1', - 'default_key1' => 'val2' + 'default_key1' => 'val2', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'default_key1' => 'val2' + ] + ] + ] + ], + [ + 4 => [ + 'key1' => '1', + 'values' => [3 => ['key1' => 1]] ] ], - [4 => ['key1' => '1']], [ [ 'option_id' => '3', 'key1' => 'val1', - 'default_key1' => 'val2' + 'default_key1' => 'val2', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'default_key1' => 'val2' + ] + ] ] ] ], @@ -342,19 +364,44 @@ public function mergeProductOptionsDataProvider() [ 'option_id' => '5', 'key1' => 'val1', - 'key2' => 'val2', + 'title' => 'val2', 'default_key1' => 'val3', - 'default_key2' => 'val4' + 'default_title' => 'val4', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'key2' => 'val2', + 'default_key1' => 'val11', + 'default_key2' => 'val22' + ] + ] + ] + ], + [ + 5 => [ + 'key1' => '0', + 'title' => '1', + 'values' => [2 => ['key1' => 1]] ] ], - [5 => ['key1' => '0', 'key2' => '1']], [ [ 'option_id' => '5', 'key1' => 'val1', - 'key2' => 'val4', + 'title' => 'val4', 'default_key1' => 'val3', - 'default_key2' => 'val4' + 'default_title' => 'val4', + 'is_delete_store_title' => 1, + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val11', + 'key2' => 'val2', + 'default_key1' => 'val11', + 'default_key2' => 'val22' + ] + ] ] ] ], @@ -364,16 +411,41 @@ public function mergeProductOptionsDataProvider() 'option_id' => '7', 'key1' => 'val1', 'key2' => 'val2', - 'default_key1' => 'val3' + 'default_key1' => 'val3', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'title' => 'val2', + 'default_key1' => 'val11', + 'default_title' => 'val22' + ] + ] + ] + ], + [ + 7 => [ + 'key1' => '1', + 'key2' => '1', + 'values' => [2 => ['key1' => 0, 'title' => 1]] ] ], - [7 => ['key1' => '1', 'key2' => '1']], [ [ 'option_id' => '7', 'key1' => 'val3', 'key2' => 'val2', - 'default_key1' => 'val3' + 'default_key1' => 'val3', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'title' => 'val22', + 'default_key1' => 'val11', + 'default_title' => 'val22', + 'is_delete_store_title' => 1 + ] + ] ] ], ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php index b88e71cfdafdf..5818da0057401 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php @@ -7,8 +7,13 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Option; -use \Magento\Catalog\Model\Product\Option\Repository; +use Magento\Catalog\Model\Product\Option\Repository; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RepositoryTest extends \PHPUnit_Framework_TestCase { /** @@ -27,10 +32,17 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase protected $optionResourceMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ProductCustomOptionInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $optionMock; + /** + * Catalog product options collection factory. + * + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $optionCollectionFactory; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -38,35 +50,44 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->productRepositoryMock = $this->getMock('Magento\Catalog\Model\ProductRepository', [], [], '', false); + $this->productRepositoryMock = $this->getMock( + \Magento\Catalog\Model\ProductRepository::class, + [], + [], + '', + false + ); $this->optionResourceMock = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\Option', + \Magento\Catalog\Model\ResourceModel\Product\Option::class, [], [], '', false ); - $this->converterMock = $this->getMock('\Magento\Catalog\Model\Product\Option\Converter', [], [], '', false); - $this->optionMock = $this->getMock('\Magento\Catalog\Model\Product\Option', [], [], '', false); - $this->productMock = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); - $optionFactory = $this->getMock( - 'Magento\Catalog\Model\Product\OptionFactory', - ['create'], + $this->converterMock = $this->getMock( + \Magento\Catalog\Model\Product\Option\Converter::class, + [], [], '', false ); - $optionCollectionFactory = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory', + $this->optionMock = $this->getMock(\Magento\Catalog\Model\Product\Option::class, [], [], '', false); + $this->productMock = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $optionFactory = $this->getMock( + \Magento\Catalog\Model\Product\OptionFactory::class, ['create'], [], '', false ); - $metadataPool = $this->getMockBuilder('Magento\Framework\EntityManager\MetadataPool') + $this->optionCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() ->getMock(); - $metadata = $this->getMockBuilder('Magento\Framework\EntityManager\EntityMetadata') + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) ->disableOriginalConstructor() ->getMock(); $metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); @@ -81,7 +102,7 @@ protected function setUp() $this->optionRepository, [ 'optionFactory' => $optionFactory, - 'optionCollectionFactory' => $optionCollectionFactory, + 'collectionFactory' => $this->optionCollectionFactory, 'metadataPool' => $metadataPool ] ); @@ -225,4 +246,75 @@ private function setProperties($object, $properties = []) } } } + + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage ProductSku should be specified + */ + public function testSaveCouldNotSaveException() + { + $this->optionMock->expects($this->once())->method('getProductSku')->willReturn(null); + $this->optionRepository->save($this->optionMock); + } + + /** + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + */ + public function testSaveNoSuchEntityException() + { + $productSku = 'simple_product'; + $optionId = 1; + $productOptionId = 2; + $this->optionMock->expects($this->once())->method('getProductSku')->willReturn($productSku); + $this->productRepositoryMock + ->expects($this->once()) + ->method('get') + ->with($productSku) + ->willReturn($this->productMock); + $productOption = clone $this->optionMock; + $this->optionMock->expects($this->any())->method('getOptionId')->willReturn($optionId); + $productOption->expects($this->any())->method('getOptionId')->willReturn($productOptionId); + $this->productMock->expects($this->once())->method('getOptions')->willReturn([$productOption]); + $this->optionRepository->save($this->optionMock); + } + + public function testSave() + { + $productSku = 'simple_product'; + $optionId = 1; + $originalValue1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) + ->disableOriginalConstructor() + ->getMock(); + $originalValue2 = clone $originalValue1; + $originalValue3 = clone $originalValue1; + + $originalValue1->expects($this->at(0))->method('getData')->with('option_type_id')->willReturn(10); + $originalValue1->expects($this->once())->method('setData')->with('is_delete', 1); + $originalValue2->expects($this->once())->method('getData')->with('option_type_id')->willReturn(4); + $originalValue3->expects($this->once())->method('getData')->with('option_type_id')->willReturn(5); + + $this->optionMock->expects($this->once())->method('getProductSku')->willReturn($productSku); + $this->productRepositoryMock + ->expects($this->once()) + ->method('get') + ->with($productSku) + ->willReturn($this->productMock); + $this->optionMock->expects($this->any())->method('getOptionId')->willReturn($optionId); + $this->productMock->expects($this->once())->method('getOptions')->willReturn([]); + $this->optionMock->expects($this->once())->method('getData')->with('values')->willReturn([ + ['option_type_id' => 4], + ['option_type_id' => 5] + ]); + $optionCollection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Option\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $optionCollection->expects($this->once())->method('getProductOptions')->willReturn([$this->optionMock]); + $this->optionCollectionFactory->expects($this->once())->method('create')->willReturn($optionCollection); + $this->optionMock->expects($this->once())->method('getValues')->willReturn([ + $originalValue1, + $originalValue2, + $originalValue3 + ]); + $this->assertEquals($this->optionMock, $this->optionRepository->save($this->optionMock)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php new file mode 100644 index 0000000000000..fee9f54ac981f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php @@ -0,0 +1,75 @@ +entity = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionMock = $this->getMockBuilder(Option::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionRepository = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new SaveHandler($this->optionRepository); + } + + public function testExecute() + { + $this->optionMock->expects($this->any())->method('getOptionId')->willReturn(5); + $this->entity->expects($this->once())->method('getOptions')->willReturn([$this->optionMock]); + + $secondOptionMock = $this->getMockBuilder(Option::class) + ->disableOriginalConstructor() + ->getMock(); + $secondOptionMock->expects($this->once())->method('getOptionId')->willReturn(6); + + $this->optionRepository + ->expects($this->once()) + ->method('getProductOptions') + ->with($this->entity) + ->willReturn([$this->optionMock, $secondOptionMock]); + + $this->optionRepository->expects($this->once())->method('delete'); + $this->optionRepository->expects($this->once())->method('save')->with($this->optionMock); + + $this->assertEquals($this->entity, $this->model->execute($this->entity)); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php index 248ca8b21a344..5b663363921cb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php @@ -87,35 +87,47 @@ public function testModifyData() $originalData = [ $productId => [ - \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions::DATA_SOURCE_DEFAULT => [ + CustomOptions::DATA_SOURCE_DEFAULT => [ 'title' => 'original' ] ] ]; $options = [ - $this->getProductOptionMock(['title' => 'option1']), + $this->getProductOptionMock(['title' => 'option1', 'store_title' => 'Option Store Title']), $this->getProductOptionMock( - ['title' => 'option2'], + ['title' => 'option2', 'store_title' => null], [ - $this->getProductOptionMock(['title' => 'value1']), - $this->getProductOptionMock(['title' => 'value2']) + $this->getProductOptionMock(['title' => 'value1', 'store_title' => 'Option Value Store Title']), + $this->getProductOptionMock(['title' => 'value2', 'store_title' => null]) ] ) ]; $resultData = [ $productId => [ - \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions::DATA_SOURCE_DEFAULT => [ - 'title' => 'original', - \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions::FIELD_ENABLE => 1, - \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions::GRID_OPTIONS_NAME => [ - ['title' => 'option1'], + CustomOptions::DATA_SOURCE_DEFAULT => [ + CustomOptions::FIELD_TITLE_NAME => 'original', + CustomOptions::FIELD_ENABLE => 1, + CustomOptions::GRID_OPTIONS_NAME => [ [ - 'title' => 'option2', + CustomOptions::FIELD_TITLE_NAME => 'option1', + CustomOptions::FIELD_STORE_TITLE_NAME => 'Option Store Title', + CustomOptions::FIELD_IS_USE_DEFAULT => false + ], [ + CustomOptions::FIELD_TITLE_NAME => 'option2', + CustomOptions::FIELD_STORE_TITLE_NAME => null, + CustomOptions::FIELD_IS_USE_DEFAULT => true, CustomOptions::GRID_TYPE_SELECT_NAME => [ - ['title' => 'value1'], - ['title' => 'value2'] + [ + CustomOptions::FIELD_TITLE_NAME => 'value1', + CustomOptions::FIELD_STORE_TITLE_NAME => 'Option Value Store Title', + CustomOptions::FIELD_IS_USE_DEFAULT => false + ], [ + CustomOptions::FIELD_TITLE_NAME => 'value2', + CustomOptions::FIELD_STORE_TITLE_NAME => null, + CustomOptions::FIELD_IS_USE_DEFAULT => true + ] ] ] ] @@ -154,13 +166,13 @@ public function testModifyMeta() */ protected function getProductOptionMock(array $data, array $values = []) { + /** @var ProductOption|\PHPUnit_Framework_MockObject_MockObject $productOptionMock */ $productOptionMock = $this->getMockBuilder(ProductOption::class) ->disableOriginalConstructor() + ->setMethods(['getValues']) ->getMock(); - $productOptionMock->expects($this->any()) - ->method('getData') - ->willReturn($data); + $productOptionMock->setData($data); $productOptionMock->expects($this->any()) ->method('getValues') ->willReturn($values); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 16002c9c01786..a2319b53f24e3 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php @@ -68,6 +68,7 @@ class CustomOptions extends AbstractModifier const FIELD_ENABLE = 'affect_product_custom_options'; const FIELD_OPTION_ID = 'option_id'; const FIELD_TITLE_NAME = 'title'; + const FIELD_STORE_TITLE_NAME = 'store_title'; const FIELD_TYPE_NAME = 'type'; const FIELD_IS_REQUIRE_NAME = 'is_require'; const FIELD_SORT_ORDER_NAME = 'sort_order'; @@ -79,6 +80,7 @@ class CustomOptions extends AbstractModifier const FIELD_IMAGE_SIZE_X_NAME = 'image_size_x'; const FIELD_IMAGE_SIZE_Y_NAME = 'image_size_y'; const FIELD_IS_DELETE = 'is_delete'; + const FIELD_IS_USE_DEFAULT = 'is_use_default'; /**#@-*/ /**#@+ @@ -112,7 +114,7 @@ class CustomOptions extends AbstractModifier * @var UrlInterface */ protected $urlBuilder; - + /** * @var ArrayManager */ @@ -162,9 +164,15 @@ public function modifyData(array $data) /** @var \Magento\Catalog\Model\Product\Option $option */ foreach ($productOptions as $index => $option) { - $options[$index] = $this->formatPriceByPath(static::FIELD_PRICE_NAME, $option->getData()); + $optionData = $option->getData(); + $optionData[static::FIELD_IS_USE_DEFAULT] = !$option->getData(static::FIELD_STORE_TITLE_NAME); + $options[$index] = $this->formatPriceByPath(static::FIELD_PRICE_NAME, $optionData); $values = $option->getValues() ?: []; + foreach ($values as $value) { + $value->setData(static::FIELD_IS_USE_DEFAULT, !$value->getData(static::FIELD_STORE_TITLE_NAME)); + } + /** @var \Magento\Catalog\Model\Product\Option $value */ foreach ($values as $value) { $options[$index][static::GRID_TYPE_SELECT_NAME][] = $this->formatPriceByPath( @@ -386,6 +394,7 @@ protected function getOptionsGridConfig($sortOrder) 'data' => [ 'config' => [ 'componentType' => Fieldset::NAME, + 'collapsible' => true, 'label' => null, 'sortOrder' => 10, 'opened' => true, @@ -477,7 +486,6 @@ protected function getImportOptionsModalConfig() 'ns' => static::CUSTOM_OPTIONS_LISTING, 'render_url' => $this->urlBuilder->getUrl('mui/index/render'), 'realTimeLink' => true, - 'behaviourType' => 'edit', 'externalFilterMode' => false, 'currentProductId' => $this->locator->getProduct()->getId(), 'dataLinks' => [ @@ -529,7 +537,8 @@ protected function getCommonContainerConfig($sortOrder) 'component' => 'Magento_Catalog/component/static-type-input', 'valueUpdate' => 'input', 'imports' => [ - 'optionId' => '${ $.provider }:${ $.parentScope }.option_id' + 'optionId' => '${ $.provider }:${ $.parentScope }.option_id', + 'isUseDefault' => '${ $.provider }:${ $.parentScope }.is_use_default' ] ], ], @@ -597,6 +606,23 @@ protected function getStaticTypeContainerConfig($sortOrder) */ protected function getSelectTypeGridConfig($sortOrder) { + $options = [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'imports' => [ + 'optionId' => '${ $.provider }:${ $.parentScope }.option_id', + 'optionTypeId' => '${ $.provider }:${ $.parentScope }.option_type_id', + 'isUseDefault' => '${ $.provider }:${ $.parentScope }.is_use_default' + ], + 'service' => [ + 'template' => 'Magento_Catalog/form/element/helper/custom-option-type-service', + ], + ], + ], + ], + ]; + return [ 'arguments' => [ 'data' => [ @@ -626,7 +652,10 @@ protected function getSelectTypeGridConfig($sortOrder) ], ], 'children' => [ - static::FIELD_TITLE_NAME => $this->getTitleFieldConfig(10), + static::FIELD_TITLE_NAME => $this->getTitleFieldConfig( + 10, + $this->locator->getProduct()->getStoreId() ? $options : [] + ), static::FIELD_PRICE_NAME => $this->getPriceFieldConfig(20), static::FIELD_PRICE_TYPE_NAME => $this->getPriceTypeFieldConfig(30, ['fit' => true]), static::FIELD_SKU_NAME => $this->getSkuFieldConfig(40), @@ -1090,7 +1119,7 @@ private function getLocaleCurrency() } return $this->localeCurrency; } - + /** * Format price according to the locale of the currency * diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/helper/custom-option-type-service.html b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/helper/custom-option-type-service.html new file mode 100644 index 0000000000000..3c3cbbde2ab39 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/helper/custom-option-type-service.html @@ -0,0 +1,17 @@ + +

    + + +
    diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php index d476af6c4918d..9a0a4b1cba77a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php @@ -20,6 +20,13 @@ */ class Options extends Section { + /**#@+ + * Determines if we need update option or add new one. + */ + const ACTION_ADD = 'add'; + const ACTION_UPDATE = 'update'; + /**#@-*/ + /** * Custom option row. * @@ -91,6 +98,7 @@ class Options extends Section * @return $this * * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function setFieldsData(array $fields, SimpleElement $element = null) { @@ -105,7 +113,16 @@ public function setFieldsData(array $fields, SimpleElement $element = null) continue; } $options = null; - $this->_rootElement->find($this->buttonAddOption)->click(); + + $actionType = self::ACTION_ADD; + if (isset($field['action_type'])) { + $actionType = $field['action_type']; + unset($field['action_type']); + } + if ($actionType == self::ACTION_ADD) { + $this->_rootElement->find($this->buttonAddOption)->click(); + } + if (!empty($field['options'])) { $options = $field['options']; unset($field['options']); @@ -197,6 +214,11 @@ private function setOptionTypeData(array $options, $type, ElementInterface $elem } else { $currentSortOrder = 0; } + + if (!isset($option['action_type'])) { + $option['action_type'] = 'add'; + } + $optionsForm->fillOptions( $option, $element->find(sprintf($context, $key + 1)) @@ -261,6 +283,10 @@ public function getFieldsData($tabFields = null, SimpleElement $element = null) unset($field['options']); } + if (isset($field['action_type'])) { + unset($field['action_type']); + } + $rootLocator = sprintf($this->customOptionRow, $field['title']); $rootElement = $this->_rootElement->find($rootLocator, Locator::SELECTOR_XPATH); $this->waitForElementVisible($rootLocator, Locator::SELECTOR_XPATH); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.php index 88b2ec8ce4701..ccd2bf05600cf 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.php @@ -14,6 +14,13 @@ */ class DropDown extends AbstractOptions { + /**#@+ + * Determines if we need update option value or add new one. + */ + const ACTION_ADD = 'add'; + const ACTION_UPDATE = 'update'; + /**#@-*/ + /** * "Add Value" button css selector. * @@ -30,7 +37,14 @@ class DropDown extends AbstractOptions */ public function fillOptions(array $fields, SimpleElement $element = null) { - $this->_rootElement->find($this->addValueButton)->click(); + $actionType = self::ACTION_ADD; + if (isset($fields['action_type'])) { + $actionType = $fields['action_type']; + unset($fields['action_type']); + } + if ($actionType == self::ACTION_ADD) { + $this->_rootElement->find($this->addValueButton)->click(); + } return parent::fillOptions($fields, $element); } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.xml index 669ed235512b6..6ca881baa2f9d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/Type/DropDown.xml @@ -10,6 +10,11 @@ <selector>[name$="[title]"]</selector> + + [name^="options_use_default"] + css selector + checkbox + [name$="[price]"] diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml index 02254f21c8e78..041ef5accf9a3 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml @@ -188,6 +188,11 @@ input[name$="[title]"] css selector + + fieldset[data-index="container_common"] [name^="options_use_default"] + css selector + checkbox + input[name$="[is_require]"] css selector diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductCustomOptionsOnProductPage.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductCustomOptionsOnProductPage.php index b4b9cc2b947ec..84d4e01220bff 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductCustomOptionsOnProductPage.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductCustomOptionsOnProductPage.php @@ -72,18 +72,26 @@ class AssertProductCustomOptionsOnProductPage extends AbstractAssertForm protected $isPrice = true; /** - * Assertion that commodity options are displayed correctly + * Assertion that commodity options are displayed correctly. * * @param CatalogProductView $catalogProductView * @param FixtureInterface $product * @param BrowserInterface $browser + * @param FixtureInterface|null $initialProduct + * @param bool $assertInitialProduct * @return void */ public function processAssert( CatalogProductView $catalogProductView, FixtureInterface $product, - BrowserInterface $browser + BrowserInterface $browser, + FixtureInterface $initialProduct = null, + $assertInitialProduct = false ) { + if ($assertInitialProduct && $initialProduct) { + $product = $initialProduct; + } + $browser->open($_ENV['app_frontend_url'] . $product->getUrlKey() . '.html'); $actualPrice = null; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml index cf66355fb3893..dab99f0c5a2d4 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Product/CustomOptions.xml @@ -109,6 +109,22 @@ + + + No + custom option drop down updated %isolation% + update + + + No + 40 Percent updated + 0 + update + + + + + Test1 option %isolation% diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml index 1c574a888c792..1cf539069c57c 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml @@ -154,5 +154,13 @@ + + with_one_custom_option + custom + drop_down_with_one_option_percent_price_updated + true + + + diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index a70285e570435..0dc3bc4ea93e0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -10,10 +10,10 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ -$categoryLinkManagement = $objectManager->create('Magento\Catalog\Api\CategoryLinkManagementInterface'); +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); /** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); $product->isObjectNew(true); $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId(1) @@ -93,14 +93,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '3-1-select', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', @@ -116,14 +116,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '4-1-radio', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', @@ -136,7 +136,7 @@ $options = []; /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ -$customOptionFactory = $objectManager->create('Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory'); +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); foreach ($oldOptions as $option) { /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ @@ -149,7 +149,7 @@ $product->setOptions($options); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ -$productRepositoryFactory = $objectManager->create('Magento\Catalog\Api\ProductRepositoryInterface'); +$productRepositoryFactory = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); $productRepositoryFactory->save($product); $categoryLinkManagement->assignProductToCategories( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_admin_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_admin_store.php index 6fb3ba1967083..3c77a0dd22978 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_admin_store.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_admin_store.php @@ -10,15 +10,15 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ -$storeManager = $objectManager->get('Magento\Store\Model\StoreManagerInterface'); +$storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); $store = $storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE); $storeManager->setCurrentStore($store->getCode()); /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ -$categoryLinkManagement = $objectManager->create('Magento\Catalog\Api\CategoryLinkManagementInterface'); +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); /** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); $product->isObjectNew(true); $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId(1) @@ -98,14 +98,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '3-1-select', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', @@ -121,14 +121,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '4-1-radio', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', @@ -141,7 +141,7 @@ $options = []; /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ -$customOptionFactory = $objectManager->create('Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory'); +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); foreach ($oldOptions as $option) { /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ @@ -154,7 +154,7 @@ $product->setOptions($options); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ -$productRepositoryFactory = $objectManager->create('Magento\Catalog\Api\ProductRepositoryInterface'); +$productRepositoryFactory = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); $productRepositoryFactory->save($product); $categoryLinkManagement->assignProductToCategories( diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php index c615a2108851f..e132a1cefb148 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php @@ -14,20 +14,34 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$productModel = $objectManager->create('Magento\Catalog\Model\Product'); +$productModel = $objectManager->create(\Magento\Catalog\Model\Product::class); $customOptions = [ [ - 'id' => 'test_option_code_1', - 'option_id' => '0', + 'option_id' => null, 'sort_order' => '0', 'title' => 'Option 1', 'type' => 'drop_down', 'is_require' => 1, 'values' => [ - 1 => ['option_type_id' => -1, 'title' => 'Option 1 & Value 1"', 'price' => '1.00', 'price_type' => 'fixed'], - 2 => ['option_type_id' => -1, 'title' => 'Option 1 & Value 2"', 'price' => '2.00', 'price_type' => 'fixed'], - 3 => ['option_type_id' => -1, 'title' => 'Option 1 & Value 3"', 'price' => '3.00', 'price_type' => 'fixed'] + 1 => [ + 'option_type_id' => null, + 'title' => 'Option 1 & Value 1"', + 'price' => '1.00', + 'price_type' => 'fixed' + ], + 2 => [ + 'option_type_id' => null, + 'title' => 'Option 1 & Value 2"', + 'price' => '2.00', + 'price_type' => 'fixed' + ], + 3 => [ + 'option_type_id' => null, + 'title' => 'Option 1 & Value 3"', + 'price' => '3.00', + 'price_type' => 'fixed' + ] ] ], [ @@ -42,44 +56,35 @@ ], ]; -$productModel->setTypeId( - \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE -)->setId( - 1 -)->setAttributeSetId( - 4 -)->setName( - 'New Product' -)->setSku( - 'simple' -)->setPrice( - 10 -)->addData( - ['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/'] -)->setTierPrice( - [0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]] -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setWebsiteIds( - [1] -)->setCateroryIds( - [] -)->setStockData( - ['qty' => 100, 'is_in_stock' => 1] -)->setCanSaveCustomOptions( - true -)->setCategoryIds( - [333] -)->setUpSellLinkData( - [$product->getId() => ['position' => 1]] -); +$productModel->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setName('New Product') + ->setSku('simple') + ->setPrice(10) + ->addData(['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/']) + ->setTierPrice( + [ + 0 => [ + 'website_id' => 0, + 'cust_group' => 0, + 'price_qty' => 3, + 'price' => 8 + ] + ] + )->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCateroryIds([]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1]) + ->setCanSaveCustomOptions(true) + ->setCategoryIds([333]) + ->setUpSellLinkData([$product->getId() => ['position' => 1]]); $options = []; /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ -$customOptionFactory = $objectManager->create('Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory'); +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); foreach ($customOptions as $option) { /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php index 86fd9f2412d9c..7272bd16e716a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php @@ -88,8 +88,7 @@ public function testGetFinalPriceWithCustomOption() $options = $this->prepareOptions( [ [ - 'id' => 1, - 'option_id' => 0, + 'option_id' => null, 'previous_group' => 'text', 'title' => 'Test Field', 'type' => 'field', diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php index 6b97090457b78..91773ef16be55 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php @@ -105,14 +105,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '3-1-select', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', @@ -128,14 +128,14 @@ 'sort_order' => 0, 'values' => [ [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 1', 'price' => 3, 'price_type' => 'fixed', 'sku' => '4-1-radio', ], [ - 'option_type_id' => -1, + 'option_type_id' => null, 'title' => 'Option 2', 'price' => 3, 'price_type' => 'fixed', From dfb3233428c2f703e6b7b3417fa7c810a6ddbed9 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 2 Jun 2017 13:47:15 +0300 Subject: [PATCH 184/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Controller/Adminhtml/Product/Initialization/Helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index e31f1adc116d7..ac09e6306d95f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -407,7 +407,7 @@ private function getProductRepository() { if (null === $this->productRepository) { $this->productRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\ProductRepositoryInterface\Proxy::class); + ->get('\Magento\Catalog\Api\ProductRepositoryInterface\Proxy'); } return $this->productRepository; From 2c9cf7a4bd869d2458c9ca3b10bede61be719607 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 2 Jun 2017 14:09:50 +0300 Subject: [PATCH 185/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Product/Edit/Section/Options/AbstractOptions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/AbstractOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/AbstractOptions.php index b53aea7daecad..4112dffcfdfb7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/AbstractOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options/AbstractOptions.php @@ -23,6 +23,10 @@ abstract class AbstractOptions extends Section */ public function fillOptions(array $fields, SimpleElement $element = null) { + if (isset($fields['action_type'])) { + unset($fields['action_type']); + } + $element = $element === null ? $this->_rootElement : $element; $mapping = $this->dataMapping($fields); $this->_fill($mapping, $element); From afdc22d9dfb55356bcdeb9ed01be08e0f3671d06 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 2 Jun 2017 16:22:13 +0300 Subject: [PATCH 186/363] MAGETWO-69637: Fix Travis-build for 2.1.8-develop-pr16 --- .../Catalog/_files/product_without_options.php | 13 ++++++++++--- .../_files/product_without_options_rollback.php | 14 ++++++++++---- .../Model/Quote/Address/Total/ShippingTest.php | 4 ++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options.php index a2ef04fffd556..5641a55629224 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options.php @@ -5,7 +5,9 @@ */ /** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Product::class +); $product->setTypeId( 'simple' )->setId( @@ -30,6 +32,11 @@ \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH )->setStatus( \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setQty( - 100 +)->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1 + ] +)->setWeight( + 1 )->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options_rollback.php index 1f0e4b618b958..c716330c64e24 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_without_options_rollback.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry = $objectManager->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ProductRepository' -); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); try { $product = $repository->get('simple', false, null, true); $product->delete(); @@ -20,5 +20,11 @@ //Entity already deleted } +// Remove product stock registry data. +/* @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = $objectManager->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class); +$stockRegistryStorage->removeStockItem(1); +$stockRegistryStorage->removeStockStatus(1); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php index dde854febc80d..fe88e9f001f53 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php @@ -37,7 +37,7 @@ protected function setUp() * Estimate shipment for product that match salesrule with free shipping. * * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php */ public function testRuleByProductWeightWithFreeShipping() { @@ -53,7 +53,7 @@ public function testRuleByProductWeightWithFreeShipping() * Estimate shipment for product that doesn't match salesrule with free shipping. * * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php */ public function testRuleByProductWeightWithoutFreeShipping() { From 5933525bbcb741bf37b15b68585f21147c6c02b2 Mon Sep 17 00:00:00 2001 From: nmalevanec Date: Fri, 2 Jun 2017 17:28:42 +0300 Subject: [PATCH 187/363] MAGETWO-60542: [Backport] - "Print Shipping Label" link does not displays on frontend - for 2.1 --- .../blank/Magento_Rma/web/css/source/_module.less | 9 +++++---- .../Magento/luma/Magento_Rma/web/css/source/_module.less | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/design/frontend/Magento/blank/Magento_Rma/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Rma/web/css/source/_module.less index 076b698787011..7f6255ee870d1 100644 --- a/app/design/frontend/Magento/blank/Magento_Rma/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Rma/web/css/source/_module.less @@ -158,11 +158,12 @@ .block-returns-tracking { .block-title { .action { - margin: 12px 0 0 30px; + margin: 0 0 0 30px; + } - &.track { - float: right; - } + .actions-track { + float: right; + margin-top: 12px; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less index 162808c039a72..bfd84524adc6a 100644 --- a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less @@ -176,11 +176,12 @@ .block-returns-tracking { .block-title { .action { - margin: 12px 0 0 30px; + margin: 0 0 0 30px; + } - &.track { - float: right; - } + .actions-track { + float: right; + margin-top: 12px; } } } From 3cc79f3fb5c05a43863bed841fbe28c15f699d7d Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Fri, 2 Jun 2017 19:01:06 +0300 Subject: [PATCH 188/363] MAGETWO-67109: [FT] Cant click DataGrid Action if browser window is small --- .../Ui/Test/Block/Adminhtml/DataGrid.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php index 4af3f965e1adf..80b5859297693 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php @@ -346,10 +346,23 @@ public function selectMassAction($massActionSelection) public function selectAction($action) { $actionType = is_array($action) ? key($action) : $action; - $this->getGridHeaderElement()->find($this->actionButton)->click(); - $this->getGridHeaderElement() - ->find(sprintf($this->actionList, $actionType), Locator::SELECTOR_XPATH) - ->click(); + // Find Action button (dropdown actually) + $actionButton = $this->getGridHeaderElement()->find($this->actionButton); + // Click it to show options (actions) + $actionButton->click(); + // Find needed element (action) to click on + $actionElement = $this->getGridHeaderElement() + ->find(sprintf($this->actionList, $actionType), Locator::SELECTOR_XPATH); + // Scroll to show it (action option) on viewport. It can be out of viewport because of small window height. + $actionElement->hover(); + // In case of small window after scroll to action element it may became hidden + // It because of appearance special top-stick panel of actions + // So we need to click action button again to show list of actions + if (!$actionElement->isVisible()) { + $actionButton->click(); + } + // Click on action element to run appropriate command + $actionElement->click(); if (is_array($action)) { $this->getGridHeaderElement() ->find(sprintf($this->actionList, end($action)), Locator::SELECTOR_XPATH) From 1db97fb6771cffd1c12c598e2ffbca52f5222c83 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 6 Jun 2017 08:50:21 +0300 Subject: [PATCH 189/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Magento/Catalog/Model/Product/Option/Repository.php | 6 ------ .../Catalog/Model/Product/Option/Validator/Select.php | 2 -- .../Adminhtml/Product/Initialization/HelperTest.php | 2 +- .../Test/Unit/Model/Product/Option/RepositoryTest.php | 2 +- .../Product/Form/Modifier/CustomOptionsTest.php | 6 ++++-- .../CatalogImportExport/_files/product_export_data.php | 2 +- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php index 5bd162c1ffacb..9714fc8e2c715 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php @@ -135,11 +135,9 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt { /** @var string $productSku */ $productSku = $option->getProductSku(); - if (!$productSku) { throw new CouldNotSaveException(__('ProductSku should be specified')); } - /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->get($productSku); /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ @@ -148,13 +146,10 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt $option->setData('store_id', $product->getStoreId()); if ($option->getOptionId()) { - $options = $product->getOptions(); - if (!$options) { $options = $this->getProductOptions($product); } - $persistedOption = array_filter( $options, function ($iOption) use ($option) { @@ -171,7 +166,6 @@ function ($iOption) use ($option) { $originalValues = $persistedOption->getValues(); /** @var array $newValues */ $newValues = $option->getData('values'); - if ($newValues) { $newValues = $this->markRemovedValues($newValues, $originalValues); $option->setData('values', $newValues); diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php index 80e24fba67d16..9b9dc4f2582d9 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php @@ -51,11 +51,9 @@ protected function validateOptionValue(Option $option) $storeId = $option->getProduct()->getStoreId(); } foreach ($values as $value) { - if (isset($value['is_delete']) && (bool)$value['is_delete']) { continue; } - $type = isset($value['price_type']) ? $value['price_type'] : null; $price = isset($value['price']) ? $value['price'] : null; $title = isset($value['title']) ? $value['title'] : null; diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index fa60ea8ee6fd6..01e64233d7482 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -197,7 +197,7 @@ public function testInitialize() $optionsData = [ 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''], 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'], - 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'] + 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'], ]; $productData = [ 'stock_data' => ['stock_data'], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php index 5818da0057401..c09739de4f0e4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php @@ -41,7 +41,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase * * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $optionCollectionFactory; + private $optionCollectionFactory; /** * @var \PHPUnit_Framework_MockObject_MockObject diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php index 5b663363921cb..724e8855537f7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php @@ -114,7 +114,8 @@ public function testModifyData() CustomOptions::FIELD_TITLE_NAME => 'option1', CustomOptions::FIELD_STORE_TITLE_NAME => 'Option Store Title', CustomOptions::FIELD_IS_USE_DEFAULT => false - ], [ + ], + [ CustomOptions::FIELD_TITLE_NAME => 'option2', CustomOptions::FIELD_STORE_TITLE_NAME => null, CustomOptions::FIELD_IS_USE_DEFAULT => true, @@ -123,7 +124,8 @@ public function testModifyData() CustomOptions::FIELD_TITLE_NAME => 'value1', CustomOptions::FIELD_STORE_TITLE_NAME => 'Option Value Store Title', CustomOptions::FIELD_IS_USE_DEFAULT => false - ], [ + ], + [ CustomOptions::FIELD_TITLE_NAME => 'value2', CustomOptions::FIELD_STORE_TITLE_NAME => null, CustomOptions::FIELD_IS_USE_DEFAULT => true diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php index e132a1cefb148..091c44fb557d3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php @@ -41,7 +41,7 @@ 'title' => 'Option 1 & Value 3"', 'price' => '3.00', 'price_type' => 'fixed' - ] + ], ] ], [ From d1545c110ff663642aa5381da7a1f5c5fef1b887 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 6 Jun 2017 11:42:06 +0300 Subject: [PATCH 190/363] MAGETWO-62995: Product imports not Auto-Generating URL Keys for SKUs - for 2.1.x --- .../CatalogImportExport/Model/Import/ProductTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index b6ffc358aa0f5..56339e9bc16ee 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -856,7 +856,7 @@ public function testProductsWithMultipleStores() $this->_model->importData(); /** @var \Magento\Catalog\Model\Product $product */ - $product = $objectManager->create('Magento\Catalog\Model\Product'); + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); $id = $product->getIdBySku('Configurable 03'); $product->load($id); $this->assertEquals('1', $product->getHasOptions()); @@ -1276,7 +1276,7 @@ public function testProductWithLinks() $productId = $resource->getIdBySku('simple4'); /** @var \Magento\Catalog\Model\Product $product */ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class ); $product->load($productId); $productLinks = [ @@ -1326,7 +1326,7 @@ public function testExistingProductWithUrlKeys() $this->_model->importData(); $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Api\ProductRepositoryInterface' + \Magento\Catalog\Api\ProductRepositoryInterface::class ); foreach ($products as $productSku => $productUrlKey) { $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); @@ -1366,7 +1366,7 @@ public function testProductWithUseConfigSettings() foreach ($products as $sku => $manageStockUseConfig) { /** @var \Magento\CatalogInventory\Model\StockRegistry $stockRegistry */ $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\CatalogInventory\Model\StockRegistry' + \Magento\CatalogInventory\Model\StockRegistry::class ); $stockItem = $stockRegistry->getStockItemBySku($sku); $this->assertEquals($manageStockUseConfig, $stockItem->getUseConfigManageStock()); From 9b2e52082f05c7438a545b2e3fbf00951cf410e9 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 6 Jun 2017 11:48:37 +0300 Subject: [PATCH 191/363] MAGETWO-62995: Product imports not Auto-Generating URL Keys for SKUs - for 2.1.x --- app/code/Magento/CatalogImportExport/Model/Import/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 6be6df1740c94..a6c4e0f6ac37f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1043,8 +1043,8 @@ protected function _initTypeModels() throw new \Magento\Framework\Exception\LocalizedException( __( 'Entity type model must be an instance of ' - . 'Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType' ) + . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class ); } if ($model->isSuitable()) { From ba2a63bb470f0d0c4afdf1fd311666c16afb7474 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 6 Jun 2017 13:44:18 +0300 Subject: [PATCH 192/363] MAGETWO-62995: Product imports not Auto-Generating URL Keys for SKUs - for 2.1.x --- app/code/Magento/CatalogImportExport/Model/Import/Product.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index a6c4e0f6ac37f..bdc73b8fb22f7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1042,9 +1042,9 @@ protected function _initTypeModels() if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { throw new \Magento\Framework\Exception\LocalizedException( __( - 'Entity type model must be an instance of ' + "Entity type model must be an instance of '%1'", + \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class ) - . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class ); } if ($model->isSuitable()) { From ec8162f88fc370166a6a7d5e376ad219f1b95c01 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Tue, 6 Jun 2017 14:28:14 +0300 Subject: [PATCH 193/363] MAGETWO-64242: Magento\Sales\Test\TestCase\MoveShoppingCartProductsOnOrderPageTest.test with data set "MoveShoppingCartProductsOnOrderPageTestVariation2" --- .../Test/Repository/ConfigurableProduct.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml index b57afacd2c308..275f3a7dbc86c 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml @@ -62,9 +62,7 @@ In Stock - - default - + Main Website default From f11f450d7f43fe718fa9c2c44b1ce21cb7657e63 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Tue, 6 Jun 2017 19:07:03 +0300 Subject: [PATCH 194/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../Ui/Test/Block/Adminhtml/AbstractFormContainers.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/AbstractFormContainers.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/AbstractFormContainers.php index aa36c4e892143..1ed346b22431c 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/AbstractFormContainers.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/AbstractFormContainers.php @@ -44,7 +44,7 @@ abstract class AbstractFormContainers extends Form * * @var string */ - protected $closeButton = '*//aside[@style!=\'\']//button[@data-role="closeBtn"]'; + protected $closeButton = 'aside[style]:not([style=""]) [data-role="closeBtn"]'; /** * Initialize. @@ -201,8 +201,8 @@ protected function fillMissedFields() unset($this->unassignedFields[$fieldName]); } } - if ($this->browser->find($this->closeButton, Locator::SELECTOR_XPATH)->isVisible()) { - $this->browser->find($this->closeButton, Locator::SELECTOR_XPATH)->click(); + if ($this->browser->find($this->closeButton)->isVisible()) { + $this->browser->find($this->closeButton)->click(); } if (empty($this->unassignedFields)) { break; From 49555279025838900732faa057d54e75e3a3b618 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Tue, 6 Jun 2017 19:13:08 +0300 Subject: [PATCH 195/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 1fbbcb23cc43c..40c1043b486f7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -70,6 +70,7 @@ public function testModifyMeta() $this->prepareDataForComparison($actualMeta, $expectedMeta); $this->assertEquals($expectedMeta, $actualMeta); } + /** * Test modifying meta on new product. */ From d20e059382aefa9168b96c3c9db8c7f34833a30f Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Tue, 6 Jun 2017 23:42:42 +0300 Subject: [PATCH 196/363] MAGETWO-58042: [Backport] - Search fails with an error when used user-defined price attribute as searchable - for 2.1 --- .../CatalogSearch/Block/Advanced/Result.php | 11 +++ .../Model/Search/RequestGenerator.php | 69 ++++++++--------- .../Model/Search/RequestGenerator/Decimal.php | 45 +++++++++++ .../Model/Search/RequestGenerator/General.php | 43 +++++++++++ .../RequestGenerator/GeneratorInterface.php | 31 ++++++++ .../RequestGenerator/GeneratorResolver.php | 52 +++++++++++++ .../Search/RequestGenerator/DecimalTest.php | 67 ++++++++++++++++ .../Search/RequestGenerator/GeneralTest.php | 65 ++++++++++++++++ .../GeneratorResolverTest.php | 76 +++++++++++++++++++ .../Model/Search/RequestGeneratorTest.php | 57 +++++++++++--- app/code/Magento/CatalogSearch/etc/di.xml | 8 ++ .../Search/Adapter/Mysql/AdapterTest.php | 35 +++++++++ .../Search/_files/price_attribute.php | 58 ++++++++++++++ .../_files/price_attribute_rollback.php | 37 +++++++++ .../Framework/Search/_files/requests.xml | 21 +++++ 15 files changed, 624 insertions(+), 51 deletions(-) create mode 100644 app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php create mode 100644 app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php create mode 100644 app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php create mode 100644 app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php create mode 100644 app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute_rollback.php diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php index 76e15ecd6a525..c927b9ad9e39e 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php @@ -64,6 +64,7 @@ public function __construct( */ protected function _prepareLayout() { + $this->pageConfig->getTitle()->set($this->getPageTitle()); $breadcrumbs = $this->getLayout()->getBlock('breadcrumbs'); if ($breadcrumbs) { $breadcrumbs->addCrumb( @@ -84,6 +85,16 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + /** + * Get page title + * + * @return \Magento\Framework\Phrase + */ + private function getPageTitle() + { + return __('Advanced Search Results'); + } + /** * Set order options * diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 609b48956b81e..66f0f69f9f2b9 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -8,10 +8,14 @@ use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Catalog\Model\Entity\Attribute; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; -use Magento\Framework\Search\Request\BucketInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; +/** + * Search request generator. + */ class RequestGenerator { /** Filter name suffix */ @@ -25,12 +29,22 @@ class RequestGenerator */ private $productAttributeCollectionFactory; + /** + * @var GeneratorResolver + */ + private $generatorResolver; + /** * @param CollectionFactory $productAttributeCollectionFactory + * @param GeneratorResolver $generatorResolver */ - public function __construct(CollectionFactory $productAttributeCollectionFactory) - { + public function __construct( + CollectionFactory $productAttributeCollectionFactory, + GeneratorResolver $generatorResolver = null + ) { $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; + $this->generatorResolver = $generatorResolver + ?: ObjectManager::getInstance()->get(GeneratorResolver::class); } /** @@ -46,6 +60,7 @@ public function generate() $requests['quick_search_container'] = $this->generateRequest(EavAttributeInterface::IS_FILTERABLE_IN_SEARCH, 'quick_search_container', true); $requests['advanced_search_container'] = $this->generateAdvancedSearchRequest(); + return $requests; } @@ -62,7 +77,7 @@ private function generateRequest($attributeType, $container, $useFulltext) $request = []; foreach ($this->getSearchableAttributes() as $attribute) { if ($attribute->getData($attributeType)) { - if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'])) { + if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) { $queryName = $attribute->getAttributeCode() . '_query'; $request['queries'][$container]['queryReference'][] = [ @@ -76,58 +91,33 @@ private function generateRequest($attributeType, $container, $useFulltext) 'filterReference' => [['ref' => $filterName]], ]; $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; - if ($attribute->getBackendType() == 'decimal') { - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_RANGE, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'from' => '$' . $attribute->getAttributeCode() . '.from$', - 'to' => '$' . $attribute->getAttributeCode() . '.to$', - ]; - $request['aggregations'][$bucketName] = [ - 'type' => BucketInterface::TYPE_DYNAMIC, - 'name' => $bucketName, - 'field' => $attribute->getAttributeCode(), - 'method' => 'manual', - 'metric' => [["type" => "count"]], - ]; - } else { - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_TERM, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'value' => '$' . $attribute->getAttributeCode() . '$', - ]; - $request['aggregations'][$bucketName] = [ - 'type' => BucketInterface::TYPE_TERM, - 'name' => $bucketName, - 'field' => $attribute->getAttributeCode(), - 'metric' => [["type" => "count"]], - ]; - } + $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + $request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName); + $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); } } /** @var $attribute Attribute */ - if (in_array($attribute->getAttributeCode(), ['price', 'sku']) - || !$attribute->getIsSearchable() - ) { - //same fields have special semantics + if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price', 'sku'], true)) { + // Some fields have their own specific handlers continue; } - if ($useFulltext) { + + // Match search by custom price attribute isn't supported + if ($useFulltext && $attribute->getFrontendInput() !== 'price') { $request['queries']['search']['match'][] = [ 'field' => $attribute->getAttributeCode(), 'boost' => $attribute->getSearchWeight() ?: 1, ]; } } + return $request; } /** * Retrieve searchable attributes * - * @return \Magento\Catalog\Model\Entity\Attribute[] + * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */ protected function getSearchableAttributes() { @@ -231,6 +221,7 @@ private function generateAdvancedSearchRequest() ]; } } + return $request; } } diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php new file mode 100644 index 0000000000000..44842be8ca7da --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php @@ -0,0 +1,45 @@ + FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * {@inheritdoc} + */ + public function getAggregationData(Attribute $attribute, $bucketName) + { + return [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'method' => 'manual', + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php new file mode 100644 index 0000000000000..5f4efb683fb23 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php @@ -0,0 +1,43 @@ + FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * {@inheritdoc} + */ + public function getAggregationData(Attribute $attribute, $bucketName) + { + return [ + 'type' => BucketInterface::TYPE_TERM, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php new file mode 100644 index 0000000000000..a41c103100a5a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php @@ -0,0 +1,31 @@ +defaultGenerator = $defaultGenerator; + $this->generators = $generators; + } + + /** + * Return search generator for specified attribute backend type. + * + * @param string $type + * @return GeneratorInterface + * @throws \InvalidArgumentException + */ + public function getGeneratorForType($type) + { + $generator = isset($this->generators[$type]) ? $this->generators[$type] : $this->defaultGenerator; + if (!($generator instanceof GeneratorInterface)) { + throw new \InvalidArgumentException( + 'Generator must implement ' . GeneratorInterface::class + ); + } + + return $generator; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php new file mode 100644 index 0000000000000..6807bfa87d4f0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php @@ -0,0 +1,67 @@ +attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->decimal = $objectManager->getObject(Decimal::class); + } + + public function testGetFilterData() + { + $filterName = 'test_filter_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attributeCode, + 'from' => '$' . $attributeCode . '.from$', + 'to' => '$' . $attributeCode . '.to$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->decimal->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attributeCode, + 'method' => 'manual', + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->decimal->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php new file mode 100644 index 0000000000000..cf4f7f1401c56 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php @@ -0,0 +1,65 @@ +attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->general = $objectManager->getObject(General::class); + } + + public function testGetFilterData() + { + $filterName = 'test_general_filter_name'; + $attributeCode = 'test_general_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attributeCode, + 'value' => '$' . $attributeCode . '$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->general->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => BucketInterface::TYPE_TERM, + 'name' => $bucketName, + 'field' => $attributeCode, + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->general->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php new file mode 100644 index 0000000000000..93c6e81aece4f --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php @@ -0,0 +1,76 @@ +defaultGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->datetimeGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->rangeGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $invalidTypeGenerator = $this->getMockBuilder(\stdClass::class) + ->setMethods([]); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->resolver = $objectManager->getObject( + GeneratorResolver::class, + [ + 'defaultGenerator' => $this->defaultGenerator, + 'generators' => [ + 'datetime' => $this->datetimeGenerator, + 'range' => $this->datetimeGenerator, + 'invalid_type' => $invalidTypeGenerator, + ], + ] + ); + } + + public function testGetSpecificGenerator() + { + $this->assertEquals($this->rangeGenerator, $this->resolver->getGeneratorForType('range')); + $this->assertEquals($this->datetimeGenerator, $this->resolver->getGeneratorForType('datetime')); + } + + public function testGetFallbackGenerator() + { + $this->assertEquals($this->defaultGenerator, $this->resolver->getGeneratorForType('unknown_type')); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetInvalidGeneratorType() + { + $this->resolver->getGeneratorForType('invalid_type'); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php index 6d9d46905db6a..1f1ccba8f1444 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php @@ -6,6 +6,8 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Search; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; class RequestGeneratorTest extends \PHPUnit_Framework_TestCase { @@ -21,15 +23,33 @@ class RequestGeneratorTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->productAttributeCollectionFactory = - $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory') + $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); + $generatorResolver = $this->getMockBuilder(GeneratorResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getGeneratorForType']) + ->getMock(); + $generator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods(['getFilterData', 'getAggregationData']) + ->getMockForAbstractClass(); + $generator->expects($this->any()) + ->method('getFilterData') + ->willReturn(['some filter data goes here']); + $generator->expects($this->any()) + ->method('getAggregationData') + ->willReturn(['some aggregation data goes here']); + $generatorResolver->method('getGeneratorForType') + ->willReturn($generator); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->object = $this->objectManagerHelper->getObject( - 'Magento\\CatalogSearch\\Model\\Search\\RequestGenerator', - ['productAttributeCollectionFactory' => $this->productAttributeCollectionFactory] + \Magento\CatalogSearch\Model\Search\RequestGenerator::class, + [ + 'productAttributeCollectionFactory' => $this->productAttributeCollectionFactory, + 'generatorResolver' => $generatorResolver + ] ); } @@ -87,6 +107,14 @@ public function attributesProvider() ], ['attr_int', 'int', 0, 1, 0] ], + [ + [ + 'quick_search_container' => ['queries' => 2, 'filters' => 1, 'aggregations' => 1], + 'advanced_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], + 'catalog_view_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], + ], + ['custom_price_attr', 'price', 0, 1, 0], + ], ]; } @@ -97,7 +125,7 @@ public function attributesProvider() */ public function testGenerate($countResult, $attributeOptions) { - $collection = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection') + $collection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class) ->disableOriginalConstructor() ->getMock(); $collection->expects($this->any()) @@ -124,19 +152,23 @@ public function testGenerate($countResult, $attributeOptions) $this->assertEquals( $countResult['quick_search_container']['queries'], - $this->countVal($result['quick_search_container']['queries']) + $this->countVal($result['quick_search_container']['queries']), + 'Queries count for "quick_search_container" doesn\'t match' ); $this->assertEquals( $countResult['advanced_search_container']['queries'], - $this->countVal($result['advanced_search_container']['queries']) + $this->countVal($result['advanced_search_container']['queries']), + 'Queries count for "advanced_search_container" doesn\'t match' ); $this->assertEquals( $countResult['advanced_search_container']['filters'], - $this->countVal($result['advanced_search_container']['filters']) + $this->countVal($result['advanced_search_container']['filters']), + 'Filters count for "advanced_search_container" doesn\'t match' ); $this->assertEquals( $countResult['catalog_view_container']['queries'], - $this->countVal($result['catalog_view_container']['queries']) + $this->countVal($result['catalog_view_container']['queries']), + 'Queries count for "catalog_view_container" doesn\'t match' ); } @@ -144,11 +176,12 @@ public function testGenerate($countResult, $attributeOptions) * Create attribute mock * * @param $attributeOptions - * @return \PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Catalog\Model\Entity\Attribute|\PHPUnit_Framework_MockObject_MockObject */ private function createAttributeMock($attributeOptions) { - $attribute = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute') + /** @var \Magento\Catalog\Model\Entity\Attribute|\PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->setMethods( [ @@ -184,8 +217,8 @@ private function createAttributeMock($attributeOptions) ->method('getData') ->willReturnMap( [ - ['is_filterable', $attributeOptions[2]], - ['is_filterable_in_search', $attributeOptions[3]] + ['is_filterable', null, $attributeOptions[2]], + ['is_filterable_in_search', null, $attributeOptions[3]], ] ); diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 3e5e949aa8b45..25993adf3530c 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -238,4 +238,12 @@ + + + \Magento\CatalogSearch\Model\Search\RequestGenerator\General + + Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal + + + 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 a5b24354ff831..717fdbd4bc2df 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 @@ -449,6 +449,9 @@ public function testAdvancedSearchConfigProductWithOutOfStockOption() $this->assertEquals(1, $queryResponse->count()); } + /** + * @return array + */ public function dateDataProvider() { return [ @@ -458,4 +461,36 @@ public function dateDataProvider() [['from' => '2000-02-01T00:00:00Z', 'to' => ''], 0], ]; } + + /** + * Search request using custom price attribute. + * + * @param $rangeFilter + * @param $expectedRecordsCount + * @magentoDataFixture Magento/Framework/Search/_files/price_attribute.php + * @magentoConfigFixture current_store catalog/search/engine mysql + * @dataProvider priceDataProvider + */ + public function testSearchCustomPriceField($rangeFilter, $expectedRecordsCount) + { + $this->requestBuilder->bind('price.from', $rangeFilter['from']); + $this->requestBuilder->bind('price.to', $rangeFilter['to']); + $this->requestBuilder->setRequestName('search_custom_price_field'); + + $queryResponse = $this->executeQuery(); + $this->assertEquals($expectedRecordsCount, $queryResponse->count()); + } + + /** + * @return array + */ + public function priceDataProvider() + { + return [ + [['from' => '19.8900', 'to' => '19.8900'], 1], + [['from' => '19.8900', 'to' => ''], 1], + [['from' => '19.0000', 'to' => '19.8900'], 1], + [['from' => '', 'to' => '19.8900'], 1], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute.php new file mode 100644 index 0000000000000..767ae48f3e3c9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute.php @@ -0,0 +1,58 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class, + ['resourceName' => 'catalog_setup'] +); + +/** @var $selectAttribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$priceAttribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +$priceAttribute->setData( + [ + 'attribute_code' => 'price_attribute', + 'entity_type_id' => $installer->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY), + 'is_global' => 1, + 'is_searchable' => 1, + 'is_filterable' => 1, + 'backend_type' => 'decimal', + 'frontend_input' => 'price', + 'frontend_label' => 'Test Price', + ] +); +$priceAttribute->save(); + +$setId = $installer->getDefaultAttributeSetId(\Magento\Catalog\Model\Product::ENTITY); +$groupId = $installer->getDefaultAttributeGroupId(\Magento\Catalog\Model\Product::ENTITY, $setId); + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', $setId, $groupId, $priceAttribute->getId()); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product + ->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($setId) + ->setWebsiteIds([1]) + ->setName('Simple Product with custom price attribute') + ->setSku('simple_product_with_custom_price_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::class) + ->updateAttributes( + [$product->getId()], + [$priceAttribute->getAttributeCode() => '19.89'], + \Magento\Store\Model\Store::DEFAULT_STORE_ID + ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute_rollback.php new file mode 100644 index 0000000000000..6c4c6d9b0ebe4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/price_attribute_rollback.php @@ -0,0 +1,37 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class, + ['resourceName' => 'catalog_setup'] +); +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product = $product->loadByAttribute('sku', 'simple_product_with_custom_price_attribute'); +if ($product->getId()) { + $product->delete(); +} + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +$attribute->loadByCode( + $installer->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY), + 'price_attribute' +); +if ($attribute->getId()) { + $attribute->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); 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 25d667d0d547c..11daaba314f53 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml @@ -406,4 +406,25 @@ 0 10 + + + + + + + + + + + + + + + + 0 + 10 + From 82a2a6fb75896235b9be30816abb4b87cd82a740 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 7 Jun 2017 13:55:23 +0300 Subject: [PATCH 197/363] MAGETWO-61135: [Backport] - Unable to add video to product via REST api - for 2.1 --- .../Catalog/Model/ProductRepository.php | 39 +++++++++++----- ...uteMediaGalleryManagementInterfaceTest.php | 45 +++++++++++++++++++ 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 1d0bc6e866ea1..c8ba17ff966e2 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -278,6 +278,7 @@ protected function getCacheKey($data) */ protected function initializeProductData(array $productData, $createNew) { + unset($productData['media_gallery']); if ($createNew) { $product = $this->productFactory->create(); if ($this->storeManager->hasSingleStore()) { @@ -420,8 +421,15 @@ private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $produc } /** - * @param ProductInterface $product - * @param array $mediaGalleryEntries + * Process Media gallery data before save product. + * + * Compare Media Gallery Entries Data with existing Media Gallery + * * If Media entry has not value_id set it as new + * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag + * * Merge Existing and new media gallery + * + * @param ProductInterface $product contains only existing media gallery items + * @param array $mediaGalleryEntries array which contains all media gallery items * @return $this * @throws InputException * @throws StateException @@ -431,11 +439,10 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE { $existingMediaGallery = $product->getMediaGallery('images'); $newEntries = []; + $entriesById = []; if (!empty($existingMediaGallery)) { - $entriesById = []; foreach ($mediaGalleryEntries as $entry) { - if (isset($entry['id'])) { - $entry['value_id'] = $entry['id']; + if (isset($entry['value_id'])) { $entriesById[$entry['value_id']] = $entry; } else { $newEntries[] = $entry; @@ -444,6 +451,9 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE foreach ($existingMediaGallery as $key => &$existingEntry) { if (isset($entriesById[$existingEntry['value_id']])) { $updatedEntry = $entriesById[$existingEntry['value_id']]; + if ($updatedEntry['file'] === null) { + unset($updatedEntry['file']); + } $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); } else { //set the removed flag @@ -471,11 +481,18 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE } /** @var ImageContentInterface $contentDataObject */ $contentDataObject = $this->contentFactory->create() - ->setName($newEntry['content'][ImageContentInterface::NAME]) - ->setBase64EncodedData($newEntry['content'][ImageContentInterface::BASE64_ENCODED_DATA]) - ->setType($newEntry['content'][ImageContentInterface::TYPE]); + ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) + ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) + ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); $newEntry['content'] = $contentDataObject; $this->processNewMediaGalleryEntry($product, $newEntry); + + $finalGallery = $product->getData('media_gallery'); + $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); + $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); + $entriesById[$newEntryId] = $newEntry; + $finalGallery['images'][$newEntryId] = $newEntry; + $product->setData('media_gallery', $finalGallery); } return $this; } @@ -503,8 +520,6 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $productDataArray = $this->extensibleDataObjectConverter ->toNestedArray($product, [], 'Magento\Catalog\Api\Data\ProductInterface'); $productDataArray = array_replace($productDataArray, $product->getData()); - unset($productDataArray['media_gallery']); - $ignoreLinksFlag = $product->getData('ignore_links_flag'); $productLinks = null; if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { @@ -514,8 +529,8 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); - if (isset($productDataArray['media_gallery_entries'])) { - $this->processMediaGallery($product, $productDataArray['media_gallery_entries']); + if (isset($productDataArray['media_gallery'])) { + $this->processMediaGallery($product, $productDataArray['media_gallery']['images']); } if (!$product->getOptionsReadonly()) { diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php index 84ab2cf5f1974..538cc3e71967f 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php @@ -615,4 +615,49 @@ public function testGetListForAbsentSku() } $this->_webApiCall($serviceInfo, $requestData); } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testAddProductVideo() + { + $videoContent = [ + 'media_type' => 'external-video', + 'video_provider' => 'vimeo', + 'video_url' => 'https://vimeo.com/testUrl', + 'video_title' => 'Vimeo Test Title', + 'video_description' => 'test description', + 'video_metadata' => 'video meta data' + ]; + + $requestData = [ + 'id' => null, + 'media_type' => 'external-video', + 'label' => 'Image Text', + 'position' => 1, + 'types' => null, + 'disabled' => false, + 'content' => [ + ImageContentInterface::BASE64_ENCODED_DATA => base64_encode(file_get_contents($this->testImagePath)), + ImageContentInterface::TYPE => 'image/jpeg', + ImageContentInterface::NAME => 'test_image.jpg' + ], + 'extension_attributes' => [ + 'video_content' => $videoContent + ] + ]; + + $actualResult = $this->_webApiCall($this->createServiceInfo, ['sku' => 'simple', 'entry' => $requestData]); + $targetProduct = $this->getTargetSimpleProduct(); + $mediaGallery = $targetProduct->getData('media_gallery'); + + $this->assertCount(1, $mediaGallery['images']); + $updatedImage = array_shift($mediaGallery['images']); + $this->assertEquals($actualResult, $updatedImage['value_id']); + $this->assertEquals('Image Text', $updatedImage['label']); + $this->assertEquals(1, $updatedImage['position']); + $this->assertEquals(0, $updatedImage['disabled']); + $this->assertStringStartsWith('/t/e/test_image', $updatedImage['file']); + $this->assertEquals($videoContent, array_intersect($updatedImage, $videoContent)); + } } From 8e1dadd8872d3ec9202f296468ca5c33f293ac3f Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Wed, 7 Jun 2017 17:23:17 +0300 Subject: [PATCH 198/363] MAGETWO-67101: [FT] AdminUiControllerPermissionTest fail on variation AdminUiControllerPermissionTestV1 on Jenkins for 2.1.7 (CE) --- .../Magento/Ui/Test/Constraint/AssertAdminUrlNoAccess.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Constraint/AssertAdminUrlNoAccess.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Constraint/AssertAdminUrlNoAccess.php index 210e293dd1d2f..36c6ee79ca74a 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Constraint/AssertAdminUrlNoAccess.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Constraint/AssertAdminUrlNoAccess.php @@ -28,13 +28,10 @@ class AssertAdminUrlNoAccess extends AbstractConstraint * * @var array */ - private $urls = [ + protected $urls = [ 'mui/index/render/?namespace=cms_block_listing&search=&filters%5Bplaceholder%5D=true' . '&paging%5BpageSize%5D=20&paging%5Bcurrent%5D=1&sorting%5Bfield%5D=block_id' . '&sorting%5Bdirection%5D=asc&isAjax=true', - 'mui/index/render/handle/bulk_bulk_details_modal/buttons/1/?namespace=support_report_listing' - . '&filters%5Bplaceholder%5D=true&paging%5BpageSize%5D=20&paging%5Bcurrent%5D=1&sorting%5Bfield%5D=report_id' - . '&sorting%5Bdirection%5D=asc&isAjax=true', 'mui/index/render/?namespace=customer_listing&search=&filters%5Bplaceholder%5D=true' . '&paging%5BpageSize%5D=20&paging%5Bcurrent%5D=1&sorting%5Bfield%5D=entity_id&sorting%5Bdirection%5D=asc' . '&isAjax=true', From 54632c04039cc31760351713de19f27e91682d28 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 8 Jun 2017 10:14:02 +0300 Subject: [PATCH 199/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Product/Initialization/HelperTest.php | 338 +++++++++++------- 1 file changed, 218 insertions(+), 120 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 8bc397b301df5..b4ab0d3914e3b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -6,10 +6,12 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; -use Magento\Catalog\Api\ProductRepositoryInterface as ProductRepository; +use Magento\Catalog\Api\Data\ProductLinkTypeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; @@ -17,10 +19,8 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\DateTime\Filter\Date as DateFilter; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; -use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; use Magento\Catalog\Model\Product\LinkTypeProvider; -use Magento\Catalog\Api\Data\ProductLinkTypeInterface; use Magento\Catalog\Model\ProductLink\Link as ProductLink; /** @@ -98,31 +98,31 @@ class HelperTest extends \PHPUnit_Framework_TestCase protected $customOptionFactoryMock; /** - * @var ProductCustomOptionInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Option|\PHPUnit_Framework_MockObject_MockObject */ protected $customOptionMock; /** - * @var \Magento\Catalog\Model\Product\Link\Resolver|\PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject */ protected $linkResolverMock; /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider|\PHPUnit_Framework_MockObject_MockObject + * @var ProductLinks|\PHPUnit_Framework_MockObject_MockObject */ - protected $linkTypeProviderMock; + protected $productLinksMock; /** - * @var ProductLinks|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product\LinkTypeProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $productLinksMock; + protected $linkTypeProviderMock; protected function setUp() { $this->objectManager = new ObjectManager($this); $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) - ->setMethods(['create']) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class) ->disableOriginalConstructor() @@ -145,7 +145,6 @@ protected function setUp() ->getMock(); $this->productMock = $this->getMockBuilder(Product::class) ->setMethods([ - 'setData', 'addData', 'getId', 'setWebsiteIds', @@ -154,7 +153,6 @@ protected function setUp() 'getAttributes', 'unlockAttribute', 'getOptionsReadOnly', - 'setOptions', 'setCanSaveCustomOptions', '__sleep', '__wakeup', @@ -167,9 +165,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->customOptionMock = $this->getMockBuilder(ProductCustomOptionInterface::class) + $this->customOptionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->setMethods(null) + ->getMock(); $this->productLinksMock = $this->getMockBuilder(ProductLinks::class) ->disableOriginalConstructor() ->getMock(); @@ -207,19 +206,12 @@ protected function setUp() */ private function assembleProductMock($links = []) { - $this->customOptionMock->expects($this->once()) - ->method('setProductSku'); - $this->customOptionMock->expects($this->once()) - ->method('setOptionId'); - - $optionsData = [ - 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1'], - 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1'], - ]; + $optionsData = $this->getOptionsData(); $productData = [ 'stock_data' => ['stock_data'], 'options' => $optionsData, ]; + $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->getMock(); @@ -234,7 +226,6 @@ private function assembleProductMock($links = []) $attributeDateBackEnd = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\Datetime::class) ->disableOriginalConstructor() ->getMock(); - $attributeNonDate->expects($this->any()) ->method('getBackend') ->willReturn($attributeNonDateBackEnd); @@ -247,7 +238,6 @@ private function assembleProductMock($links = []) $attributeDateBackEnd->expects($this->any()) ->method('getType') ->willReturn('datetime'); - $attributesArray = [ $attributeNonDate, $attributeDate @@ -296,26 +286,49 @@ private function assembleProductMock($links = []) ->method('getOptionsReadOnly') ->willReturn(false); + $customOptionMockClone1 = clone $this->customOptionMock; + $customOptionMockClone2 = clone $this->customOptionMock; + $this->customOptionFactoryMock->expects($this->any()) ->method('create') - ->with(['data' => $optionsData['option2']]) - ->willReturn($this->customOptionMock); - $this->productMock->expects($this->once()) - ->method('setOptions') - ->with([$this->customOptionMock]); + ->willReturnMap([ + [ + ['data' => $optionsData['option2']], + $customOptionMockClone1->setData($optionsData['option2']) + ], [ + ['data' => $optionsData['option3']], + $customOptionMockClone2->setData($optionsData['option3']) + ] + ]); } /** * @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::initialize + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testInitialize() { $this->assembleProductMock(); + + $this->productMock->expects($this->any()) + ->method('getProductLinks') + ->willReturn([]); + + $this->linkTypeProviderMock->expects($this->once()) ->method('getItems') ->willReturn($this->assembleLinkTypes(['related', 'upsell', 'crosssell'])); $this->assertEquals($this->productMock, $this->helper->initialize($this->productMock)); + $optionsData = $this->getOptionsData(); + + $productOptions = $this->productMock->getOptions(); + $this->assertTrue(2 == count($productOptions)); + list($option2, $option3) = $productOptions; + $this->assertTrue($option2->getOptionId() == $optionsData['option2']['option_id']); + $this->assertTrue('sku' == $option2->getData('product_sku')); + $this->assertTrue($option3->getOptionId() == $optionsData['option3']['option_id']); + $this->assertTrue('sku' == $option2->getData('product_sku')); } /** @@ -356,6 +369,167 @@ public function testInitializeWithLinks($links, $linkTypes, $expectedLinks) } /** + * Data provider for testMergeProductOptions. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function mergeProductOptionsDataProvider() + { + return [ + 'options are not array, empty array is returned' => [ + null, + [], + [], + ], + 'replacement is not array, original options are returned' => [ + ['val'], + null, + ['val'], + ], + 'ids do not match, no replacement occurs' => [ + [ + [ + 'option_id' => '3', + 'key1' => 'val1', + 'default_key1' => 'val2', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'default_key1' => 'val2' + ] + ] + ] + ], + [ + 4 => [ + 'key1' => '1', + 'values' => [3 => ['key1' => 1]] + ] + ], + [ + [ + 'option_id' => '3', + 'key1' => 'val1', + 'default_key1' => 'val2', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'default_key1' => 'val2' + ] + ] + ] + ] + ], + 'key2 is replaced, key1 is not (checkbox is not checked)' => [ + [ + [ + 'option_id' => '5', + 'key1' => 'val1', + 'title' => 'val2', + 'default_key1' => 'val3', + 'default_title' => 'val4', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'key2' => 'val2', + 'default_key1' => 'val11', + 'default_key2' => 'val22' + ] + ] + ] + ], + [ + 5 => [ + 'key1' => '0', + 'title' => '1', + 'values' => [2 => ['key1' => 1]] + ] + ], + [ + [ + 'option_id' => '5', + 'key1' => 'val1', + 'title' => 'val4', + 'default_key1' => 'val3', + 'default_title' => 'val4', + 'is_delete_store_title' => 1, + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val11', + 'key2' => 'val2', + 'default_key1' => 'val11', + 'default_key2' => 'val22' + ] + ] + ] + ] + ], + 'key1 is replaced, key2 has no default value' => [ + [ + [ + 'option_id' => '7', + 'key1' => 'val1', + 'key2' => 'val2', + 'default_key1' => 'val3', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'title' => 'val2', + 'default_key1' => 'val11', + 'default_title' => 'val22' + ] + ] + ] + ], + [ + 7 => [ + 'key1' => '1', + 'key2' => '1', + 'values' => [2 => ['key1' => 0, 'title' => 1]] + ] + ], + [ + [ + 'option_id' => '7', + 'key1' => 'val3', + 'key2' => 'val2', + 'default_key1' => 'val3', + 'values' => [ + [ + 'option_type_id' => '2', + 'key1' => 'val1', + 'title' => 'val22', + 'default_key1' => 'val11', + 'default_title' => 'val22', + 'is_delete_store_title' => 1 + ] + ] + ] + ], + ], + ]; + } + + /** + * @param array $productOptions + * @param array $defaultOptions + * @param array $expectedResults + * @dataProvider mergeProductOptionsDataProvider + */ + public function testMergeProductOptions($productOptions, $defaultOptions, $expectedResults) + { + $result = $this->helper->mergeProductOptions($productOptions, $defaultOptions); + $this->assertEquals($expectedResults, $result); + } + + /** + * Data provider for testInitializeWithLinks. * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -490,96 +664,6 @@ public function initializeWithLinksDataProvider() ]; } - /** - * Data provider for testMergeProductOptions - * - * @return array - */ - public function mergeProductOptionsDataProvider() - { - return [ - 'options are not array, empty array is returned' => [ - null, - [], - [], - ], - 'replacement is not array, original options are returned' => [ - ['val'], - null, - ['val'], - ], - 'ids do not match, no replacement occurs' => [ - [ - [ - 'option_id' => '3', - 'key1' => 'val1', - 'default_key1' => 'val2' - ] - ], - [4 => ['key1' => '1']], - [ - [ - 'option_id' => '3', - 'key1' => 'val1', - 'default_key1' => 'val2' - ] - ] - ], - 'key2 is replaced, key1 is not (checkbox is not checked)' => [ - [ - [ - 'option_id' => '5', - 'key1' => 'val1', - 'key2' => 'val2', - 'default_key1' => 'val3', - 'default_key2' => 'val4' - ] - ], - [5 => ['key1' => '0', 'key2' => '1']], - [ - [ - 'option_id' => '5', - 'key1' => 'val1', - 'key2' => 'val4', - 'default_key1' => 'val3', - 'default_key2' => 'val4' - ] - ] - ], - 'key1 is replaced, key2 has no default value' => [ - [ - [ - 'option_id' => '7', - 'key1' => 'val1', - 'key2' => 'val2', - 'default_key1' => 'val3' - ] - ], - [7 => ['key1' => '1', 'key2' => '1']], - [ - [ - 'option_id' => '7', - 'key1' => 'val3', - 'key2' => 'val2', - 'default_key1' => 'val3' - ] - ], - ], - ]; - } - - /** - * @param array $productOptions - * @param array $defaultOptions - * @param array $expectedResults - * @dataProvider mergeProductOptionsDataProvider - */ - public function testMergeProductOptions($productOptions, $defaultOptions, $expectedResults) - { - $result = $this->helper->mergeProductOptions($productOptions, $defaultOptions); - $this->assertEquals($expectedResults, $result); - } - /** * @param array $types * @return array @@ -630,4 +714,18 @@ private function assembleProductRepositoryMock($links) ->method('getById') ->will($this->returnValueMap($repositoryReturnMap)); } + + /** + * @return array + */ + private function getOptionsData() + { + $optionsData = [ + 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''], + 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'], + 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'], + ]; + + return $optionsData; + } } From 06e916d1ddc581877527f314e27fa3df6f6e7d57 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 8 Jun 2017 10:51:58 +0300 Subject: [PATCH 200/363] MAGETWO-69474: [FT] CreateProductAttributeEntityFromProductPageTest (CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting) unstable for 2.1.7 (CE) --- .../Catalog/Test/Block/Adminhtml/Product/ProductForm.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php index 9f214b4017424..9fca21779b231 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.php @@ -151,7 +151,7 @@ public function checkAttributeLabel($productAttribute) public function getAttributesSearchGrid() { return $this->blockFactory->create( - '\Magento\Catalog\Test\Block\Adminhtml\Product\Edit\Section\Attributes\Grid', + \Magento\Catalog\Test\Block\Adminhtml\Product\Edit\Section\Attributes\Grid::class, ['element' => $this->browser->find($this->attributeSearch)] ); } @@ -190,7 +190,7 @@ public function openCustomSection($sectionName) public function getAttributeForm() { return $this->blockFactory->create( - 'Magento\Catalog\Test\Block\Adminhtml\Product\Attribute\AttributeForm', + \Magento\Catalog\Test\Block\Adminhtml\Product\Attribute\AttributeForm::class, ['element' => $this->browser->find($this->newAttributeModal)] ); } @@ -206,7 +206,7 @@ public function getAttributeElement(CatalogProductAttribute $attribute) return $this->_rootElement->find( sprintf($this->attributeBlock, $attribute->getAttributeCode()), Locator::SELECTOR_CSS, - 'Magento\Catalog\Test\Block\Adminhtml\Product\Attribute\CustomAttribute' + \Magento\Catalog\Test\Block\Adminhtml\Product\Attribute\CustomAttribute::class ); } } From ba74de2b5947d224f03c73f38ffb0735cb2f2f1d Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 8 Jun 2017 11:19:56 +0300 Subject: [PATCH 201/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Magento/Catalog/Model/Indexer/Category/ProductTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php index 8476391673284..5aa226fb59a57 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php @@ -100,8 +100,6 @@ public function testCategoryMove() /** @var Category $categorySecond */ $categorySecond = $categories[1]; - $categorySecond->setIsAnchor(true); - $categorySecond->save(); /** @var Category $categoryThird */ $categoryThird = $categories[2]; From a2b8a72129c15e80adbff9b3dde96406de316492 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 8 Jun 2017 12:21:32 +0300 Subject: [PATCH 202/363] MAGETWO-57153: [Backport] - [Github] Custom options not displayed correctly on a store view level #2908 #5885 - for 2.1 --- .../Controller/Adminhtml/Product/Initialization/HelperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index b4ab0d3914e3b..ca7bc41c41288 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -314,7 +314,6 @@ public function testInitialize() ->method('getProductLinks') ->willReturn([]); - $this->linkTypeProviderMock->expects($this->once()) ->method('getItems') ->willReturn($this->assembleLinkTypes(['related', 'upsell', 'crosssell'])); From 960ba65d4f2f791410d3ec48d534c43e71868b28 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 8 Jun 2017 12:21:57 +0300 Subject: [PATCH 203/363] MAGETWO-58042: [Backport] - Search fails with an error when used user-defined price attribute as searchable - for 2.1 --- .../CatalogSearch/Block/Advanced/Result.php | 30 ++++++--- .../Model/Search/RequestGenerator.php | 12 ++-- .../RequestGenerator/GeneratorInterface.php | 6 +- .../RequestGenerator/GeneratorResolver.php | 4 ++ .../Search/RequestGenerator/DecimalTest.php | 16 +++++ .../Search/RequestGenerator/GeneralTest.php | 16 +++++ .../GeneratorResolverTest.php | 20 +++++- .../Model/Search/RequestGeneratorTest.php | 17 +++++- .../Search/Adapter/Mysql/AdapterTest.php | 61 ++++++++++++++----- 9 files changed, 152 insertions(+), 30 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php index c927b9ad9e39e..36e60fe4626af 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php @@ -14,26 +14,26 @@ use Magento\Framework\View\Element\Template\Context; /** - * Advanced search result + * Advanced search result. */ class Result extends Template { /** - * Url factory + * Url factory. * * @var UrlFactory */ protected $_urlFactory; /** - * Catalog layer + * Catalog layer. * * @var \Magento\Catalog\Model\Layer */ protected $_catalogLayer; /** - * Catalog search advanced + * Catalog search advanced. * * @var Advanced */ @@ -82,11 +82,12 @@ protected function _prepareLayout() ['label' => __('Results')] ); } + return parent::_prepareLayout(); } /** - * Get page title + * Get page title. * * @return \Magento\Framework\Phrase */ @@ -96,7 +97,7 @@ private function getPageTitle() } /** - * Set order options + * Set order options. * * @return void */ @@ -112,7 +113,7 @@ public function setListOrders() } /** - * Set view mode options + * Set view mode options. * * @return void */ @@ -122,6 +123,8 @@ public function setListModes() } /** + * Set search result collection. + * * @return void */ public function setListCollection() @@ -130,6 +133,8 @@ public function setListCollection() } /** + * Return product collection. + * * @return Collection */ protected function _getProductCollection() @@ -138,6 +143,8 @@ protected function _getProductCollection() } /** + * Return search model. + * * @return Advanced */ public function getSearchModel() @@ -146,6 +153,8 @@ public function getSearchModel() } /** + * Return results count. + * * @return mixed */ public function getResultCount() @@ -154,10 +163,13 @@ public function getResultCount() $size = $this->getSearchModel()->getProductCollection()->getSize(); $this->setResultCount($size); } + return $this->getData('result_count'); } /** + * Return search product listing html. + * * @return string */ public function getProductListHtml() @@ -166,6 +178,8 @@ public function getProductListHtml() } /** + * Return form url. + * * @return string */ public function getFormUrl() @@ -179,6 +193,8 @@ public function getFormUrl() } /** + * Return search criteria. + * * @return array */ public function getSearchCriterias() diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 66f0f69f9f2b9..9a16f95a17847 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -25,11 +25,15 @@ class RequestGenerator const BUCKET_SUFFIX = '_bucket'; /** + * Product attribute collection factory. + * * @var CollectionFactory */ private $productAttributeCollectionFactory; /** + * Search generator resolver. + * * @var GeneratorResolver */ private $generatorResolver; @@ -48,7 +52,7 @@ public function __construct( } /** - * Generate dynamic fields requests + * Generate dynamic fields requests. * * @return array */ @@ -65,7 +69,7 @@ public function generate() } /** - * Generate search request + * Generate search request. * * @param string $attributeType * @param string $container @@ -115,7 +119,7 @@ private function generateRequest($attributeType, $container, $useFulltext) } /** - * Retrieve searchable attributes + * Retrieve searchable attributes. * * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */ @@ -132,7 +136,7 @@ protected function getSearchableAttributes() } /** - * Generate advanced search request + * Generate advanced search request. * * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php index a41c103100a5a..d4099966befbc 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php @@ -14,7 +14,8 @@ interface GeneratorInterface { /** - * Get filter data for specific attribute + * Get filter data for specific attribute. + * * @param Attribute $attribute * @param string $filterName * @return array @@ -22,7 +23,8 @@ interface GeneratorInterface public function getFilterData(Attribute $attribute, $filterName); /** - * Get aggregation data for specific attribute + * Get aggregation data for specific attribute. + * * @param Attribute $attribute * @param string $bucketName * @return array diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php index e7754289731fe..8b1b38a0e5afa 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php @@ -12,11 +12,15 @@ class GeneratorResolver { /** + * Search request generators. + * * @var GeneratorInterface[] */ private $generators; /** + * Default search request generator. + * * @var GeneratorInterface */ private $defaultGenerator; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php index 6807bfa87d4f0..f699257358dd4 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php @@ -11,6 +11,9 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Test for Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal. + */ class DecimalTest extends \PHPUnit_Framework_TestCase { /** @var Decimal */ @@ -19,6 +22,9 @@ class DecimalTest extends \PHPUnit_Framework_TestCase /** @var Attribute|\PHPUnit_Framework_MockObject_MockObject */ private $attribute; + /** + * {@inheritdoc} + */ protected function setUp() { $this->attribute = $this->getMockBuilder(Attribute::class) @@ -29,6 +35,11 @@ protected function setUp() $this->decimal = $objectManager->getObject(Decimal::class); } + /** + * Tests retrieving filter data by search request generator. + * + * @return void + */ public function testGetFilterData() { $filterName = 'test_filter_name'; @@ -47,6 +58,11 @@ public function testGetFilterData() $this->assertEquals($expected, $actual); } + /** + * Test retrieving aggregation data by search request generator. + * + * @return void + */ public function testGetAggregationData() { $bucketName = 'test_bucket_name'; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php index cf4f7f1401c56..1c111374fe5fa 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php @@ -11,6 +11,9 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Test for Magento\CatalogSearch\Model\Search\RequestGenerator\General. + */ class GeneralTest extends \PHPUnit_Framework_TestCase { /** @var General */ @@ -19,6 +22,9 @@ class GeneralTest extends \PHPUnit_Framework_TestCase /** @var Attribute|\PHPUnit_Framework_MockObject_MockObject */ private $attribute; + /** + * {@inheritdoc} + */ protected function setUp() { $this->attribute = $this->getMockBuilder(Attribute::class) @@ -29,6 +35,11 @@ protected function setUp() $this->general = $objectManager->getObject(General::class); } + /** + * Tests retrieving filter data by search request generator. + * + * @return void + */ public function testGetFilterData() { $filterName = 'test_general_filter_name'; @@ -46,6 +57,11 @@ public function testGetFilterData() $this->assertEquals($expected, $actual); } + /** + * Test retrieving aggregation data by search request generator. + * + * @return void + */ public function testGetAggregationData() { $bucketName = 'test_bucket_name'; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php index 93c6e81aece4f..97f7a5567fb88 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php @@ -6,10 +6,12 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator; - use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; +/** + * Test for Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver. + */ class GeneratorResolverTest extends \PHPUnit_Framework_TestCase { /** @var GeneratorResolver */ @@ -24,6 +26,9 @@ class GeneratorResolverTest extends \PHPUnit_Framework_TestCase /** @var GeneratorInterface|\PHPUnit_Framework_MockObject_MockObject */ private $rangeGenerator; + /** + * {@inheritdoc} + */ protected function setUp() { $this->defaultGenerator = $this->getMockBuilder(GeneratorInterface::class) @@ -55,19 +60,32 @@ protected function setUp() ); } + /** + * Tests resolving type specific search generator. + * + * @return void + */ public function testGetSpecificGenerator() { $this->assertEquals($this->rangeGenerator, $this->resolver->getGeneratorForType('range')); $this->assertEquals($this->datetimeGenerator, $this->resolver->getGeneratorForType('datetime')); } + /** + * Tests resolving fallback search generator. + * + * @return void + */ public function testGetFallbackGenerator() { $this->assertEquals($this->defaultGenerator, $this->resolver->getGeneratorForType('unknown_type')); } /** + * Tests resolving search generator with invalid type. + * * @expectedException InvalidArgumentException + * @return void */ public function testGetInvalidGeneratorType() { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php index 1f1ccba8f1444..0d3069163946c 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php @@ -9,6 +9,9 @@ use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; +/** + * Test for Magento\CatalogSearch\Model\Search\RequestGenerator. + */ class RequestGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -20,6 +23,9 @@ class RequestGeneratorTest extends \PHPUnit_Framework_TestCase /** @var CollectionFactory | \PHPUnit_Framework_MockObject_MockObject */ protected $productAttributeCollectionFactory; + /** + * {@inheritdoc} + */ protected function setUp() { $this->productAttributeCollectionFactory = @@ -119,8 +125,11 @@ public function attributesProvider() } /** + * Tests generate dynamic fields requests. + * * @param array $countResult * @param $attributeOptions + * @return void * @dataProvider attributesProvider */ public function testGenerate($countResult, $attributeOptions) @@ -173,7 +182,7 @@ public function testGenerate($countResult, $attributeOptions) } /** - * Create attribute mock + * Create attribute mock. * * @param $attributeOptions * @return \Magento\Catalog\Model\Entity\Attribute|\PHPUnit_Framework_MockObject_MockObject @@ -229,6 +238,12 @@ private function createAttributeMock($attributeOptions) return $attribute; } + /** + * Return count. + * + * @param array|\Countable $value + * @return int + */ private function countVal(&$value) { return !empty($value) ? count($value) : 0; 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 717fdbd4bc2df..913f2dcd58299 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 @@ -10,7 +10,7 @@ use Magento\TestFramework\Helper\Bootstrap; /** - * Class AdapterTest + * Class AdapterTest. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoDbIsolation disabled @@ -63,7 +63,7 @@ protected function setUp() } /** - * Get request config path + * Get request config path. * * @return string */ @@ -73,7 +73,9 @@ protected function getRequestConfigPath() } /** - * Make sure that correct engine is set + * Make sure that correct engine is set. + * + * @return void */ protected function assertPreConditions() { @@ -104,8 +106,11 @@ private function executeQuery() } /** + * Assert response product Ids match expected. + * * @param \Magento\Framework\Search\Response\QueryResponse $queryResponse * @param array $expectedIds + * @return void */ private function assertProductIds($queryResponse, $expectedIds) { @@ -120,7 +125,10 @@ private function assertProductIds($queryResponse, $expectedIds) } /** + * Match query test. + * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testMatchQuery() { @@ -133,7 +141,10 @@ public function testMatchQuery() } /** + * Query aggregation test. + * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testAggregationsQuery() { @@ -150,7 +161,10 @@ public function testAggregationsQuery() } /** + * Match query filter test. + * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testMatchQueryFilters() { @@ -165,9 +179,10 @@ public function testMatchQueryFilters() } /** - * Range filter test with all fields filled + * Range filter test with all fields filled. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testRangeFilterWithAllFields() { @@ -180,9 +195,10 @@ public function testRangeFilterWithAllFields() } /** - * Range filter test with all fields filled + * Range filter test without from field filled. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testRangeFilterWithoutFromField() { @@ -194,9 +210,10 @@ public function testRangeFilterWithoutFromField() } /** - * Range filter test with all fields filled + * Range filter test without to field filled. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testRangeFilterWithoutToField() { @@ -208,9 +225,10 @@ public function testRangeFilterWithoutToField() } /** - * Term filter test + * Term filter test. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testTermFilter() { @@ -223,9 +241,10 @@ public function testTermFilter() } /** - * Term filter test + * Term filter test. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testTermFilterArray() { @@ -237,9 +256,10 @@ public function testTermFilterArray() } /** - * Term filter test + * Wildcard filter test. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testWildcardFilter() { @@ -253,9 +273,10 @@ public function testWildcardFilter() } /** - * Request limits test + * Request limits test. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testSearchLimit() { @@ -269,9 +290,10 @@ public function testSearchLimit() } /** - * Bool filter test + * Bool filter test. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testBoolFilter() { @@ -292,9 +314,10 @@ public function testBoolFilter() } /** - * Test bool filter with nested negative bool filter + * Test bool filter with nested negative bool filter. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testBoolFilterWithNestedNegativeBoolFilter() { @@ -310,9 +333,10 @@ public function testBoolFilterWithNestedNegativeBoolFilter() } /** - * Test range inside nested negative bool filter + * Test range inside nested negative bool filter. * * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() { @@ -327,10 +351,11 @@ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() } /** - * Sample Advanced search request test + * Sample Advanced search request test. * * @magentoConfigFixture current_store catalog/search/engine mysql * @dataProvider advancedSearchDataProvider + * @return void */ public function testSimpleAdvancedSearch( $nameQuery, @@ -365,8 +390,11 @@ public function advancedSearchDataProvider() } /** + * Custom filterable attributes test. + * * @magentoDataFixture Magento/Framework/Search/_files/filterable_attribute.php * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testCustomFilterableAttribute() { @@ -396,10 +424,11 @@ public function testCustomFilterableAttribute() } /** - * Advanced search request using date product attribute + * Advanced search request using date product attribute. * * @param $rangeFilter * @param $expectedRecordsCount + * @return void * @magentoDataFixture Magento/Framework/Search/_files/date_attribute.php * @magentoConfigFixture current_store catalog/search/engine mysql * @dataProvider dateDataProvider @@ -419,6 +448,7 @@ public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount) * * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php * @magentoConfigFixture current_store catalog/search/engine mysql + * @return void */ public function testAdvancedSearchConfigProductWithOutOfStockOption() { @@ -467,6 +497,7 @@ public function dateDataProvider() * * @param $rangeFilter * @param $expectedRecordsCount + * @return void * @magentoDataFixture Magento/Framework/Search/_files/price_attribute.php * @magentoConfigFixture current_store catalog/search/engine mysql * @dataProvider priceDataProvider From 26f30a4bc3f18da01e42c6d0b1a4d853a2cac0fc Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 8 Jun 2017 14:10:55 +0300 Subject: [PATCH 204/363] MAGETWO-64240: Magento\Catalog\Test\TestCase\Product\AddCompareProductsTest.test with data set "AddCompareProductsTestVariation3" --- .../Test/Repository/ConfigurableProduct.xml | 4 +--- .../TestSuite/InjectableTests/MAGETWO-64240.xml | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64240.xml diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml index b57afacd2c308..275f3a7dbc86c 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml @@ -62,9 +62,7 @@ In Stock - - default - + Main Website default diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64240.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64240.xml new file mode 100644 index 0000000000000..b092f0ac4e480 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/MAGETWO-64240.xml @@ -0,0 +1,15 @@ + + + + + + + + + From d2bf058e5c11ded6b9356ee4d7a62e395e251a7f Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 8 Jun 2017 14:39:13 +0300 Subject: [PATCH 205/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Ui/DataProvider/Product/Form/Modifier/EavTest.php | 10 +++++----- .../Eav/Model/Entity/Attribute/AbstractAttribute.php | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index cfe6598540154..1f7e3c2a8941d 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -349,7 +349,7 @@ protected function createModel() 'attributeRepository' => $this->attributeRepositoryMock, 'arrayManager' => $this->arrayManagerMock, 'eavAttributeFactory' => $this->eavAttributeFactoryMock, - '_eventManager' => $this->eventManagerMock + '_eventManager' => $this->eventManagerMock, ]); } @@ -543,7 +543,7 @@ public function setupAttributeMetaDataProvider() 'source' => 'product-details', 'scopeLabel' => '', 'globalScope' => false, - 'sortOrder' => 0 + 'sortOrder' => 0, ], ], 'default_null_prod_not_new_and_not_required' => [ @@ -562,7 +562,7 @@ public function setupAttributeMetaDataProvider() 'source' => 'product-details', 'scopeLabel' => '', 'globalScope' => false, - 'sortOrder' => 0 + 'sortOrder' => 0, ], ], 'default_null_prod_new_and_not_required' => [ @@ -581,7 +581,7 @@ public function setupAttributeMetaDataProvider() 'source' => 'product-details', 'scopeLabel' => '', 'globalScope' => false, - 'sortOrder' => 0 + 'sortOrder' => 0, ], ], 'default_null_prod_new_and_required' => [ @@ -600,7 +600,7 @@ public function setupAttributeMetaDataProvider() 'source' => 'product-details', 'scopeLabel' => '', 'globalScope' => false, - 'sortOrder' => 0 + 'sortOrder' => 0, ], ] ]; diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index a6dca137b9f7d..ca8e761578565 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -122,7 +122,7 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens 'datetime', 'varchar', 'text', - 'static' + 'static', ]; /** @@ -188,7 +188,7 @@ public function __construct( */ protected function _construct() { - $this->_init('Magento\Eav\Model\ResourceModel\Entity\Attribute'); + $this->_init(\Magento\Eav\Model\ResourceModel\Entity\Attribute::class); } /** @@ -1091,7 +1091,7 @@ public function setOptions(array $options = null) foreach ($options as $option) { $optionData = $this->dataObjectProcessor->buildOutputDataArray( $option, - '\Magento\Eav\Api\Data\AttributeOptionInterface' + \Magento\Eav\Api\Data\AttributeOptionInterface::class ); $optionDataArray[] = $optionData; } @@ -1117,7 +1117,7 @@ protected function convertToObjects(array $options) $this->dataObjectHelper->populateWithArray( $optionDataObject, $option, - '\Magento\Eav\Api\Data\AttributeOptionInterface' + \Magento\Eav\Api\Data\AttributeOptionInterface::class ); $dataObjects[] = $optionDataObject; } From e75cda1cc76f070caf26b1c4af30f06e3cc95ae5 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 8 Jun 2017 14:46:00 +0300 Subject: [PATCH 206/363] MAGETWO-57144: [Backport] - Unable to assign blank value to attribute #3545 #4910 #5485 - for 2.1 --- .../Catalog/Model/Indexer/Category/ProductTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php index 5aa226fb59a57..5776d9b3698e9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php @@ -30,13 +30,13 @@ protected function setUp() { /** @var \Magento\Framework\Indexer\IndexerInterface indexer */ $this->indexer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Indexer\Model\Indexer' + ґMagento\Indexer\Model\Indexer::class ); $this->indexer->load('catalog_category_product'); /** @var \Magento\Catalog\Model\ResourceModel\Product $productResource */ $this->productResource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Catalog\Model\ResourceModel\Product' + \Magento\Catalog\Model\ResourceModel\Product::class ); } @@ -171,7 +171,7 @@ public function testCategoryCreate() /** @var Category $categorySixth */ $categorySixth = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Category' + \Magento\Catalog\Model\Category::class ); $categorySixth->setName( 'Category 6' @@ -209,7 +209,7 @@ protected function getCategories($count) { /** @var Category $category */ $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Category' + \Magento\Catalog\Model\Category::class ); $result = $category->getCollection()->getItems(); @@ -226,7 +226,7 @@ protected function getProducts($count) { /** @var \Magento\Catalog\Model\Product $product */ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Product' + \Magento\Catalog\Model\Product::class ); $result[] = $product->load(1); From 2976e01d505506400125595fd99afeb4692ba816 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Thu, 8 Jun 2017 15:33:46 +0300 Subject: [PATCH 207/363] MAGETWO-64240: Magento\Catalog\Test\TestCase\Product\AddCompareProductsTest.test with data set "AddCompareProductsTestVariation3" --- .../ConfigurableProduct/Test/Repository/ConfigurableProduct.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml index 275f3a7dbc86c..0ac06c8a06870 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct.xml @@ -1,4 +1,4 @@ - + - - - - - - - From 159a5661fdab2cd2255d2adac61d47fe965d8875 Mon Sep 17 00:00:00 2001 From: DianaRusin Date: Thu, 8 Jun 2017 18:39:12 +0300 Subject: [PATCH 212/363] MAGETWO-67115: [FT] CustomerWebapisPermissionTest fail on data set #1 on Jenkins for 2.1.7 (CE) --- .../Constraint/AssertWebapiNoAccessByCookie.php | 10 +++++++--- .../TestCase/CustomerWebapisPermissionTest.php | 15 ++++++++++----- .../TestCase/CustomerWebapisPermissionTest.xml | 2 ++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/dev/tests/functional/tests/app/Magento/Webapi/Test/Constraint/AssertWebapiNoAccessByCookie.php b/dev/tests/functional/tests/app/Magento/Webapi/Test/Constraint/AssertWebapiNoAccessByCookie.php index 08bc32db685e7..c8ae819510d58 100644 --- a/dev/tests/functional/tests/app/Magento/Webapi/Test/Constraint/AssertWebapiNoAccessByCookie.php +++ b/dev/tests/functional/tests/app/Magento/Webapi/Test/Constraint/AssertWebapiNoAccessByCookie.php @@ -41,10 +41,12 @@ public function processAssert( ) { // Create and login a customer on frontend $customer->persist(); - $this->objectManager->create( - 'Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep', + /** @var \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep $customerLoginStep */ + $customerLoginStep = $this->objectManager->create( + \Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep::class, ['customer' => $customer] - )->run(); + ); + $customerLoginStep->run(); // Go to cms page with form as logged in customer and submit request $browser->open($_ENV['app_frontend_url'] . $cms->getIdentifier()); @@ -55,6 +57,8 @@ public function processAssert( $browser->getHtmlSource(), 'Customer should not have customer webapi access through cookies.' ); + + $customerLoginStep->cleanup(); } /** diff --git a/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.php b/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.php index 0c616846968dd..6ec448a1a5093 100644 --- a/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.php +++ b/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.php @@ -82,6 +82,7 @@ public function __inject( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => 'wysiwyg_disabled'] )->run(); + return ['cmsOriginal' => $cmsOriginal]; } @@ -89,21 +90,25 @@ public function __inject( * Construct cms page with a form that contains webapi request and 'Submit Request' button. * * @param CmsPage $cmsOriginal + * @param string $url + * @param string $method * @return array */ - public function test(CmsPage $cmsOriginal) + public function test(CmsPage $cmsOriginal, $url, $method = 'POST') { $this->cmsPageIndex->open(); $this->cmsPageIndex->getCmsPageGridBlock()->searchAndOpen(['title' => $cmsOriginal->getTitle()]); $data = $cmsOriginal->getData(); - $content = '

    ' - . '

    '; + $content = << + + +HTML; $data['content'] = ['content' => $content]; $cms = $this->factory->createByCode('cmsPage', ['data' => $data]); $this->cmsPageNew->getPageForm()->fill($cms); $this->cmsPageNew->getPageMainActions()->save(); + return ['cms' => $cms]; } } diff --git a/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.xml b/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.xml index b43751d1002b4..a3939016b47b2 100644 --- a/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.xml +++ b/dev/tests/functional/tests/app/Magento/Webapi/Test/TestCase/CustomerWebapisPermissionTest.xml @@ -8,6 +8,8 @@ + rest/V1/carts/mine + POST From be09e4200fd40898512a3a0dd1ef23b96ee1eb14 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi Date: Wed, 7 Jun 2017 12:33:16 +0300 Subject: [PATCH 213/363] MAGETWO-69702: Remove ComposerLockTest for 2.1 --- .../Test/Integrity/ComposerLockTest.php | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/ComposerLockTest.php diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerLockTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerLockTest.php deleted file mode 100644 index 52b1f4fa7dce5..0000000000000 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerLockTest.php +++ /dev/null @@ -1,24 +0,0 @@ -markTestSkipped('composer.lock file doesn\'t exist'); - } - $jsonData = file_get_contents($lockFilePath); - $json = json_decode($jsonData); - $this->assertSame($hash, $json->hash, 'composer.lock file is not up to date'); - } -} From 73028cfa89dd10db6e49fa511b30ef1a744f3794 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 9 Jun 2017 09:31:02 +0300 Subject: [PATCH 214/363] MAGETWO-60538: [Backport] - [Magento Cloud] - Cache-miss when Fastly is enabled - for 2.1 --- app/code/Magento/Theme/Controller/Result/MessagePlugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php index d6b05061c8064..c320362490c7f 100644 --- a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php +++ b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php @@ -105,7 +105,6 @@ public function afterRenderResult( * ], * ] * - * * @param array $messages List of Magento messages that must be set as 'mage-messages' cookie. * @return void */ From 896dbf8f9198cfdd82e7ba97f38cabfe585796b3 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 12 Jun 2017 11:14:01 +0300 Subject: [PATCH 215/363] MAGETWO-57615: Add storeId in getting options --- .../Model/Entity/Attribute/Source/Table.php | 44 ++- .../Entity/Attribute/Source/TableTest.php | 265 ++++++++++++++---- .../AssertCategoryForAssignedProducts.php | 10 +- .../AddAttributeToAttributeSetStep.php | 18 +- .../Test/TestStep/CreateCategoryStep.php | 82 ++++++ .../TestStep/CreateProductAttributesStep.php | 102 +++++++ .../FillCustomAttributesOnProductPageStep.php | 118 ++++++++ 7 files changed, 565 insertions(+), 74 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateCategoryStep.php create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateProductAttributesStep.php create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/FillCustomAttributesOnProductPageStep.php diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index 1da1ddab5d07d..f160072735d39 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Model\Entity\Attribute\Source; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; + class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource { /** @@ -24,6 +27,13 @@ class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $_attrOptionFactory; + /** + * Store manager interface. + * + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory @@ -47,24 +57,31 @@ public function __construct( public function getAllOptions($withEmpty = true, $defaultValues = false) { $storeId = $this->getAttribute()->getStoreId(); + if ($storeId === null) { + $storeId = $this->getStoreManager()->getStore()->getId(); + } if (!is_array($this->_options)) { $this->_options = []; } if (!is_array($this->_optionsDefault)) { $this->_optionsDefault = []; } - if (!isset($this->_options[$storeId])) { + + $attributeId = $this->getAttribute()->getId(); + if (!isset($this->_options[$storeId][$attributeId])) { $collection = $this->_attrOptionCollectionFactory->create()->setPositionOrder( 'asc' )->setAttributeFilter( - $this->getAttribute()->getId() + $attributeId )->setStoreFilter( - $this->getAttribute()->getStoreId() + $storeId )->load(); - $this->_options[$storeId] = $collection->toOptionArray(); - $this->_optionsDefault[$storeId] = $collection->toOptionArray('default_value'); + $this->_options[$storeId][$attributeId] = $collection->toOptionArray(); + $this->_optionsDefault[$storeId][$attributeId] = $collection->toOptionArray('default_value'); } - $options = $defaultValues ? $this->_optionsDefault[$storeId] : $this->_options[$storeId]; + $options = $defaultValues + ? $this->_optionsDefault[$storeId][$attributeId] + : $this->_options[$storeId][$attributeId]; if ($withEmpty) { $options = $this->addEmptyOption($options); } @@ -72,6 +89,21 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) return $options; } + /** + * Get StoreManager dependency. + * + * @return StoreManagerInterface + * @deprecated + */ + private function getStoreManager() + { + if ($this->storeManager === null) { + $this->storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); + } + + return $this->storeManager; + } + /** * Retrieve Option values array by ids * diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/TableTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/TableTest.php index 38b88f44a451b..443f09e342764 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/TableTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/TableTest.php @@ -6,7 +6,17 @@ namespace Magento\Eav\Test\Unit\Model\Entity\Attribute\Source; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection as AttributeOptionCollection; +/** + * Tests \Magento\Eav\Model\Entity\Attribute\Source\Table. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TableTest extends \PHPUnit_Framework_TestCase { /** @@ -25,12 +35,37 @@ class TableTest extends \PHPUnit_Framework_TestCase */ private $attrOptionFactory; + /** + * @var AbstractSource | \PHPUnit_Framework_MockObject_MockObject + */ + private $sourceMock; + + /** + * @var AbstractAttribute | \PHPUnit_Framework_MockObject_MockObject + */ + private $abstractAttributeMock; + + /** + * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var AttributeOptionCollection|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeOptionCollectionMock; + protected function setUp() { $objectManager = new ObjectManager($this); $this->collectionFactory = $this->getMock( - 'Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory', + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory::class, [ 'create', 'setPositionOrder', @@ -45,51 +80,68 @@ protected function setUp() false ); + $this->attributeOptionCollectionMock = $this->getMockBuilder(AttributeOptionCollection::class) + ->setMethods(['toOptionArray']) + ->disableOriginalConstructor() + ->getMock(); + $this->attrOptionFactory = $this->getMockBuilder( - 'Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory' + \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory::class ) ->setMethods(['create']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->sourceMock = $this->getMockBuilder(AbstractSource::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->abstractAttributeMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getFrontend', 'getAttributeCode', '__wakeup', 'getStoreId', + 'getId', 'getIsRequired', 'getEntity', 'getBackend' + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->model = $objectManager->getObject( - 'Magento\Eav\Model\Entity\Attribute\Source\Table', + \Magento\Eav\Model\Entity\Attribute\Source\Table::class, [ 'attrOptionCollectionFactory' => $this->collectionFactory, 'attrOptionFactory' => $this->attrOptionFactory ] ); + + $this->model->setAttribute($this->abstractAttributeMock); + + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->storeMock = $this->getMockForAbstractClass(StoreInterface::class); + + $objectManager->setBackwardCompatibleProperty( + $this->model, + 'storeManager', + $this->storeManagerMock + ); } public function testGetFlatColumns() { $abstractFrontendMock = $this->getMock( - 'Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend', + \Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class, [], [], '', false ); - $abstractAttributeMock = $this->getMock( - '\Magento\Eav\Model\Entity\Attribute\AbstractAttribute', - ['getFrontend', 'getAttributeCode', '__wakeup'], - [], - '', - false - ); - - $abstractAttributeMock->expects( - $this->any() - )->method( - 'getFrontend' - )->will( - $this->returnValue($abstractFrontendMock) - ); - - $abstractAttributeMock->expects($this->any())->method('getAttributeCode')->will($this->returnValue('code')); - - $this->model->setAttribute($abstractAttributeMock); + $this->abstractAttributeMock->expects($this->any()) + ->method('getFrontend') + ->willReturn(($abstractFrontendMock)); + $this->abstractAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('code'); $flatColumns = $this->model->getFlatColumns(); @@ -118,25 +170,16 @@ public function testGetSpecificOptions($optionIds, $withEmpty) $storeId = 5; $options = [['label' => 'The label', 'value' => 'A value']]; - $attribute = $this->getMock( - 'Magento\Eav\Model\Entity\Attribute\AbstractAttribute', - ['getId', 'getStoreId', 'getIsRequired', '__wakeup'], - [], - '', - false - ); - $attribute->expects($this->once()) + $this->abstractAttributeMock->expects($this->once()) ->method('getId') ->willReturn($attributeId); - $attribute->expects($this->once()) + $this->abstractAttributeMock->expects($this->once()) ->method('getStoreId') ->willReturn($storeId); - $attribute->expects($this->any()) + $this->abstractAttributeMock->expects($this->any()) ->method('getIsRequired') ->willReturn(false); - $this->model->setAttribute($attribute); - $this->collectionFactory->expects($this->once()) ->method('create') ->willReturnSelf(); @@ -188,22 +231,14 @@ public function testGetOptionText($optionsIds, $value, $options, $expectedResult { $attributeId = 1; $storeId = 5; - $attribute = $this->getMock( - 'Magento\Eav\Model\Entity\Attribute\AbstractAttribute', - ['getId', 'getStoreId', '__wakeup'], - [], - '', - false - ); - $attribute->expects($this->once()) + + $this->abstractAttributeMock->expects($this->once()) ->method('getId') ->willReturn($attributeId); - $attribute->expects($this->once()) + $this->abstractAttributeMock->expects($this->once()) ->method('getStoreId') ->willReturn($storeId); - $this->model->setAttribute($attribute); - $this->collectionFactory->expects($this->once()) ->method('create') ->willReturnSelf(); @@ -250,36 +285,44 @@ public function testAddValueSortToCollection() { $attributeCode = 'attribute_code'; $dir = \Magento\Framework\DB\Select::SQL_ASC; - $collection = $this->getMockBuilder('Magento\Eav\Model\Entity\Collection\AbstractCollection') + $collection = $this->getMockBuilder(\Magento\Eav\Model\Entity\Collection\AbstractCollection::class) ->setMethods([ 'getSelect', 'getStoreId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attribute = $this->getMockBuilder('Magento\Eav\Model\Entity\Attribute\AbstractAttribute') + + $this->abstractAttributeMock = $this->getMockBuilder( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class + ) ->setMethods(['getAttributeCode', 'getEntity', 'getBackend', 'getId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attribute->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); - $entity = $this->getMockBuilder('Magento\Eav\Model\Entity\AbstractEntity') + $this->abstractAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); + $entity = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) ->setMethods(['getLinkField']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attribute->expects($this->once())->method('getEntity')->willReturn($entity); + + $this->abstractAttributeMock->expects($this->once())->method('getEntity')->willReturn($entity); $entity->expects($this->once())->method('getLinkField')->willReturn('entity_id'); - $select = $this->getMockBuilder('Magento\Framework\DB\Select') + $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->setMethods(['joinLeft', 'getConnection', 'order']) ->disableOriginalConstructor() ->getMock(); $collection->expects($this->any())->method('getSelect')->willReturn($select); $select->expects($this->any())->method('joinLeft')->willReturnSelf(); - $backend = $this->getMockBuilder('Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend') + $backend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) ->setMethods(['getTable']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attribute->expects($this->any())->method('getBackend')->willReturn($backend); + + $this->abstractAttributeMock->expects($this->any())->method('getBackend')->willReturn($backend); + $backend->expects($this->any())->method('getTable')->willReturn('table_name'); - $attribute->expects($this->any())->method('getId')->willReturn(1); + + $this->abstractAttributeMock->expects($this->any())->method('getId')->willReturn(1); + $collection->expects($this->once())->method('getStoreId')->willReturn(1); - $connection = $this->getMockBuilder('Magento\Framework\DB\Adapter\AdapterInterface') + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->getMock(); $expr = $this->getMockBuilder('Zend_Db_Expr') @@ -287,15 +330,119 @@ public function testAddValueSortToCollection() ->getMock(); $connection->expects($this->once())->method('getCheckSql')->willReturn($expr); $select->expects($this->once())->method('getConnection')->willReturn($connection); - $attrOption = $this->getMockBuilder('Magento\Eav\Model\ResourceModel\Entity\Attribute\Option') + $attrOption = $this->getMockBuilder(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option::class) ->disableOriginalConstructor() ->getMock(); $this->attrOptionFactory->expects($this->once())->method('create')->willReturn($attrOption); - $attrOption->expects($this->once())->method('addOptionValueToCollection')->with($collection, $attribute, $expr) + + $attrOption->expects($this->once())->method('addOptionValueToCollection') + ->with($collection, $this->abstractAttributeMock, $expr) ->willReturnSelf(); + $select->expects($this->once())->method('order')->with("{$attributeCode} {$dir}"); - $this->model->setAttribute($attribute); + $this->model->setAttribute($this->abstractAttributeMock); $this->assertEquals($this->model, $this->model->addValueSortToCollection($collection, $dir)); } + + /** + * @param bool $withEmpty + * @param bool $defaultValues + * @param array $options + * @param array $optionsDefault + * @param array $expectedResult + * + * @dataProvider getAllOptionsDataProvider + * + * @return void + */ + public function testGetAllOptions( + $withEmpty, + $defaultValues, + array $options, + array $optionsDefault, + array $expectedResult + ) { + $storeId = '1'; + $attributeId = '42'; + + $this->abstractAttributeMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(null); + + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $this->abstractAttributeMock->expects($this->once()) + ->method('getId') + ->willReturn($attributeId); + + $this->collectionFactory->expects($this->once()) + ->method('create') + ->willReturnSelf(); + $this->collectionFactory->expects($this->once()) + ->method('setPositionOrder') + ->willReturnSelf(); + $this->collectionFactory->expects($this->once()) + ->method('setAttributeFilter') + ->with($attributeId) + ->willReturnSelf(); + $this->collectionFactory->expects($this->once()) + ->method('setStoreFilter') + ->with($storeId) + ->willReturnSelf(); + $this->collectionFactory->expects($this->once()) + ->method('load') + ->willReturn($this->attributeOptionCollectionMock); + + $this->attributeOptionCollectionMock->expects($this->any()) + ->method('toOptionArray') + ->willReturnMap( + [ + ['value', $options], + ['default_value', $optionsDefault] + ] + ); + + $this->assertEquals($expectedResult, $this->model->getAllOptions($withEmpty, $defaultValues)); + } + + /** + * @return array + */ + public function getAllOptionsDataProvider() + { + return [ + [ + false, + false, + [['value' => '16', 'label' => 'black'], ['value' => '17', 'label' => 'white']], + [['value' => '16', 'label' => 'blck'], ['value' => '17', 'label' => 'wht']], + [['value' => '16', 'label' => 'black'], ['value' => '17', 'label' => 'white']] + ], + [ + false, + true, + [['value' => '16', 'label' => 'black'], ['value' => '17', 'label' => 'white']], + [['value' => '16', 'label' => 'blck'], ['value' => '17', 'label' => 'wht']], + [['value' => '16', 'label' => 'blck'], ['value' => '17', 'label' => 'wht']] + ], + [ + true, + false, + [['value' => '16', 'label' => 'black'], ['value' => '17', 'label' => 'white']], + [['value' => '16', 'label' => 'blck'], ['value' => '17', 'label' => 'wht']], + [ + ['label' => ' ', 'value' => ''], + ['value' => '16', 'label' => 'black'], + ['value' => '17', 'label' => 'white'] + ] + ] + ]; + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForAssignedProducts.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForAssignedProducts.php index d7a3e6fda8e78..4acb3c77cda69 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForAssignedProducts.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertCategoryForAssignedProducts.php @@ -23,18 +23,22 @@ class AssertCategoryForAssignedProducts extends AbstractConstraint * @param Category $category * @param CatalogCategoryView $categoryView * @param BrowserInterface $browser + * @param array $products * @return void */ public function processAssert( Category $category, CatalogCategoryView $categoryView, - BrowserInterface $browser + BrowserInterface $browser, + array $products = [] ) { $categoryUrlKey = $category->hasData('url_key') ? strtolower($category->getUrlKey()) : trim(strtolower(preg_replace('#[^0-9a-z%]+#i', '-', $category->getName())), '-'); - - $products = $category->getDataFieldConfig('category_products')['source']->getProducts(); + + if (empty($products)) { + $products = $category->getDataFieldConfig('category_products')['source']->getProducts(); + } $browser->open($_ENV['app_frontend_url'] . $categoryUrlKey . '.html'); foreach ($products as $productFixture) { diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/AddAttributeToAttributeSetStep.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/AddAttributeToAttributeSetStep.php index aaf0615b5e981..1266a7b1e4a95 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/AddAttributeToAttributeSetStep.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/AddAttributeToAttributeSetStep.php @@ -32,9 +32,9 @@ class AddAttributeToAttributeSetStep implements TestStepInterface protected $catalogProductSetEdit; /** - * Catalog Product Attribute fixture. + * Catalog Product Attribute fixtures. * - * @var CatalogProductAttribute + * @var CatalogProductAttribute[] */ protected $attribute; @@ -49,18 +49,22 @@ class AddAttributeToAttributeSetStep implements TestStepInterface * @constructor * @param CatalogProductSetIndex $catalogProductSetIndex * @param CatalogProductSetEdit $catalogProductSetEdit - * @param CatalogProductAttribute $attribute + * @param CatalogProductAttribute|array $attribute * @param CatalogAttributeSet $attributeSet */ public function __construct( CatalogProductSetIndex $catalogProductSetIndex, CatalogProductSetEdit $catalogProductSetEdit, - CatalogProductAttribute $attribute, + $attribute, CatalogAttributeSet $attributeSet ) { $this->catalogProductSetIndex = $catalogProductSetIndex; $this->catalogProductSetEdit = $catalogProductSetEdit; - $this->attribute = $attribute; + if (!is_array($attribute)) { + $this->attribute = [$attribute]; + } else { + $this->attribute = $attribute; + } $this->attributeSet = $attributeSet; } @@ -73,7 +77,9 @@ public function run() { $filterAttribute = ['set_name' => $this->attributeSet->getAttributeSetName()]; $this->catalogProductSetIndex->open()->getGrid()->searchAndOpen($filterAttribute); - $this->catalogProductSetEdit->getAttributeSetEditBlock()->moveAttribute($this->attribute->getData()); + foreach ($this->attribute as $attribute) { + $this->catalogProductSetEdit->getAttributeSetEditBlock()->moveAttribute($attribute->getData()); + } $this->catalogProductSetEdit->getPageActions()->save(); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateCategoryStep.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateCategoryStep.php new file mode 100644 index 0000000000000..c75b72ef8a6e5 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateCategoryStep.php @@ -0,0 +1,82 @@ +catalogCategoryIndex = $catalogCategoryIndex; + $this->catalogCategoryEdit = $catalogCategoryEdit; + $this->category = $category; + $this->addCategory = $addCategory; + } + + /** + * Creates category without assigned products. + * + * @return array + */ + public function run() + { + $this->catalogCategoryIndex->open(); + $this->catalogCategoryIndex->getTreeCategories()->selectCategory($this->category, false); + $typeOfAddedCategory = $this->addCategory; + $this->catalogCategoryIndex->getTreeCategories()->$typeOfAddedCategory(); + $this->catalogCategoryEdit->getEditForm()->fill($this->category); + $this->catalogCategoryEdit->getFormPageActions()->save(); + + return ['category' => $this->category]; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateProductAttributesStep.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateProductAttributesStep.php new file mode 100644 index 0000000000000..1d9296dff3b19 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/CreateProductAttributesStep.php @@ -0,0 +1,102 @@ +testStepFactory = $testStepFactory; + $this->attributes = $attribute; + $this->fixtureFactory = $fixtureFactory; + } + + /** + * Creates attributes. + * + * @return array + */ + public function run() + { + $attributes = []; + $attributeDataSets = is_array($this->attributes) ? $this->attributes : explode(',', $this->attributes); + foreach ($attributeDataSets as $key => $attributeDataSet) { + /** @var FixtureInterface[] $attributes */ + $attributes[$key] = $this->fixtureFactory->createByCode( + 'catalogProductAttribute', + ['dataset' => trim($attributeDataSet)] + ); + if ($attributes[$key]->hasData('id') === false) { + $attributes[$key]->persist(); + } + } + $this->attributesForCleanUp = $attributes; + + return ['attribute' => $attributes]; + } + + /** + * Deletes created attributes. + * + * @return void + */ + public function cleanup() + { + if (is_array($this->attributesForCleanUp)) { + foreach ($this->attributesForCleanUp as $attribute) { + $this->testStepFactory->create( + DeleteAttributeStep::class, + ['attribute' => $attribute] + )->run(); + } + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/FillCustomAttributesOnProductPageStep.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/FillCustomAttributesOnProductPageStep.php new file mode 100644 index 0000000000000..dfa32be865715 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/FillCustomAttributesOnProductPageStep.php @@ -0,0 +1,118 @@ +fixtureFactory = $fixtureFactory; + $this->productGrid = $productGrid; + $this->editProductPage = $editProductPage; + $this->attributesValue = $productAttributesValue; + $this->attributes = $attribute; + $this->products = $products; + } + + /** + * Fills custom attributes values for product on product page. + * + * @return void + */ + public function run() + { + $this->productGrid->open(); + + foreach ($this->products as $product) { + $filter = ['sku' => $product->getSku()]; + $this->productGrid->getProductGrid()->searchAndOpen($filter); + + foreach ($this->attributes as $key => $attribute) { + $customAttribute = ['value' => $this->attributesValue[$key], 'attribute' => $attribute]; + /** @var CatalogProductSimple $updatedProduct */ + $updatedProduct = $this->fixtureFactory->createByCode( + 'catalogProductSimple', + [ + 'data' => [ + 'custom_attribute' => $customAttribute + ], + ] + ); + $this->editProductPage->getProductForm()->fill($updatedProduct); + } + $this->editProductPage->getFormPageActions()->save(); + } + } +} From e2fb00b50682f1c259b88a27f21eb959e0c90c08 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 12 Jun 2017 17:03:32 +0300 Subject: [PATCH 216/363] MAGETWO-66711: Fix order grid block in functional tests --- .../Sales/Test/Block/Adminhtml/Order/Grid.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Grid.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Grid.php index 8e96036d27550..36be4e49f924d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Grid.php @@ -76,6 +76,13 @@ class Grid extends DataGrid */ protected $firstRowSelector = '//tbody/tr[1]/td[contains(@class,"data-grid-actions-cell")]/a'; + /** + * Global seach block selector. + * + * @var string + */ + private $globalSearch = '.search-global'; + /** * Start to create new order. * @@ -106,4 +113,33 @@ public function getPurchasePointStoreGroups() return $result; } + + /** + * {@inheritdoc} + */ + public function selectItems(array $items, $isSortable = true) + { + if ($isSortable) { + $this->sortGridByField('ID'); + } + foreach ($items as $item) { + //Hover on search block is made to avoid click on invisible element currentPage + $searchBlock = $this->browser->find($this->globalSearch); + $searchBlock->hover(); + $this->_rootElement->find($this->currentPage, Locator::SELECTOR_XPATH)->setValue(''); + $this->waitLoader(); + $selectItem = $this->getRow($item)->find($this->selectItem); + do { + if ($selectItem->isVisible()) { + if (!$selectItem->isSelected()) { + $selectItem->click(); + } + break; + } + } while ($this->nextPage()); + if (!$selectItem->isVisible()) { + throw new \Exception("Searched item was not found\n" . print_r($item, true)); + } + } + } } From 1ffa0583d5f3209517c48d4b609e66efdf9b82ce Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Wed, 14 Jun 2017 11:51:27 +0300 Subject: [PATCH 217/363] MAGETWO-67751: Add wait for spinner --- .../tests/app/Magento/Backend/Test/Block/Page/Header.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Block/Page/Header.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Block/Page/Header.php index b65ee683a5581..f037cbe199752 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Block/Page/Header.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Block/Page/Header.php @@ -44,6 +44,9 @@ class Header extends Block public function logOut() { if ($this->isLoggedIn()) { + $this->browser->waitUntil(function () { + return $this->browser->find('[data-role="spinner"]')->isVisible() ? null : true; + }); $this->_rootElement->find($this->adminAccountLink)->click(); $this->_rootElement->find($this->signOutLink)->click(); $this->waitForElementNotVisible($this->signOutLink); From 52bf0b84c64f5adb54d6040b7ab555c215a39b09 Mon Sep 17 00:00:00 2001 From: Oleksii Korshenko Date: Thu, 15 Jun 2017 16:05:15 -0500 Subject: [PATCH 218/363] MAGETWO-69689: Update CONTRIBUTING.md and README.md of Magento 2 CE repository --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0634653275a9a..2839ac5ee9d32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,3 +29,8 @@ If you are a new GitHub user, we recommend that you create your own [free github 3. Create and test your work. 4. Fork the Magento 2 repository according to [Fork a repository instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow [Create a pull request instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#pull_request). 5. Once your contribution is received, Magento 2 development team will review the contribution and collaborate with you as needed to improve the quality of the contribution. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. +The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). From 3bdd1c95b07dae9347ef240b10a7e27f608dc91d Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Fri, 16 Jun 2017 10:08:57 +0300 Subject: [PATCH 219/363] MAGETWO-69501: Swatch-color was not replaced by image --- app/code/Magento/Swatches/Helper/Data.php | 9 +++++++-- app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index a05e607d5135f..aee4fe7f703c1 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -346,8 +346,13 @@ private function getAllSizeImages(ModelProduct $product, $imageFile) */ private function getSwatchAttributes(Product $product) { - $swatchAttributes = $this->swatchAttributesProvider->provide($product); - return $swatchAttributes; + $attributes = $this->swatchAttributesProvider->provide($product) ?: []; + foreach ($attributes as $attribute) { + if (!$attribute->hasData(Swatch::SWATCH_INPUT_TYPE_KEY)) { + $this->populateAdditionalDataEavAttribute($attribute); + } + } + return $attributes; } /** diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php index d843f6880b85f..f4646573dbf9c 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php @@ -552,7 +552,10 @@ public function testGetSwatchAttributesAsArray($optionsArray, $attributeData, $e $storeMock->method('getId')->willReturn($storeId); $this->storeManagerMock->method('getStore')->willReturn($storeMock); - $this->attributeMock->method('getData')->with('')->willReturn($attributeData); + $this->attributeMock + ->method('getData') + ->withConsecutive(['additional_data'], ['']) + ->will($this->onConsecutiveCalls('', $attributeData)); $sourceMock = $this->getMock( \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class, From 0dcef954eb29d062f5f8a3a4fdc41cb8ad332759 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 16 Jun 2017 18:44:45 +0300 Subject: [PATCH 220/363] MAGETWO-64499: Error during deploying using " auto_increment_increment = 3" as Mysql option --- app/code/Magento/Tax/Setup/InstallData.php | 49 ++++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Tax/Setup/InstallData.php b/app/code/Magento/Tax/Setup/InstallData.php index 02b07e29f4826..376484a2727ed 100644 --- a/app/code/Magento/Tax/Setup/InstallData.php +++ b/app/code/Magento/Tax/Setup/InstallData.php @@ -6,6 +6,8 @@ namespace Magento\Tax\Setup; +use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; @@ -22,14 +24,34 @@ class InstallData implements InstallDataInterface */ private $taxSetupFactory; + /** + * Region collection factory. + * + * @var \Magento\Directory\Model\ResourceModel\Region\CollectionFactory + */ + private $regionCollectionFactory; + + /** + * Region collection. + * + * @var \Magento\Directory\Model\ResourceModel\Region\Collection + */ + private $regionCollection; + /** * Init * * @param TaxSetupFactory $taxSetupFactory + * @param CollectionFactory $collectionFactory */ - public function __construct(TaxSetupFactory $taxSetupFactory) - { + public function __construct( + TaxSetupFactory $taxSetupFactory, + CollectionFactory $collectionFactory = null + ) { $this->taxSetupFactory = $taxSetupFactory; + $this->regionCollectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get( + \Magento\Directory\Model\ResourceModel\Region\CollectionFactory::class + ); } /** @@ -101,7 +123,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface [ 'tax_calculation_rate_id' => 1, 'tax_country_id' => 'US', - 'tax_region_id' => 12, + 'tax_region_id' => $this->getRegionId('CA'), 'tax_postcode' => '*', 'code' => 'US-CA-*-Rate 1', 'rate' => '8.2500', @@ -109,14 +131,33 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface [ 'tax_calculation_rate_id' => 2, 'tax_country_id' => 'US', - 'tax_region_id' => 43, + 'tax_region_id' => $this->getRegionId('NY'), 'tax_postcode' => '*', 'code' => 'US-NY-*-Rate 1', 'rate' => '8.3750' ], ]; + foreach ($data as $row) { $setup->getConnection()->insertForce($setup->getTable('tax_calculation_rate'), $row); } } + + /** + * Return region id by code. + * + * @param string $regionCode + * @return mixed + */ + private function getRegionId($regionCode) + { + if ($this->regionCollection === null) { + /** @var \Magento\Directory\Model\ResourceModel\Region\Collection $regionCollection */ + $this->regionCollection = $this->regionCollectionFactory->create(); + $this->regionCollection->addCountryFilter('US') + ->addRegionCodeOrNameFilter(['CA', 'NY']); + } + + return $this->regionCollection->getItemByColumnValue('code', $regionCode)->getId(); + } } From 3aa22b9ff6fc4881ea7e5a3cc21fae791762f874 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Mon, 19 Jun 2017 11:07:38 +0300 Subject: [PATCH 221/363] MAGETWO-64499: Error during deploying using " auto_increment_increment = 3" as Mysql option --- app/code/Magento/Tax/Setup/InstallData.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Tax/Setup/InstallData.php b/app/code/Magento/Tax/Setup/InstallData.php index 376484a2727ed..6869360f82cb4 100644 --- a/app/code/Magento/Tax/Setup/InstallData.php +++ b/app/code/Magento/Tax/Setup/InstallData.php @@ -147,7 +147,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface * Return region id by code. * * @param string $regionCode - * @return mixed + * @return string */ private function getRegionId($regionCode) { @@ -158,6 +158,13 @@ private function getRegionId($regionCode) ->addRegionCodeOrNameFilter(['CA', 'NY']); } - return $this->regionCollection->getItemByColumnValue('code', $regionCode)->getId(); + $regionId = ''; + /** @var $item */ + $item = $this->regionCollection->getItemByColumnValue('code', $regionCode); + if ($item) { + $regionId = $item->getId(); + } + + return $regionId; } } From 108fdb456dd02f951d8df363119c3c151eb49c2d Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Mon, 19 Jun 2017 11:11:06 +0300 Subject: [PATCH 222/363] MAGETWO-64499: Error during deploying using " auto_increment_increment = 3" as Mysql option --- app/code/Magento/Tax/Setup/InstallData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Tax/Setup/InstallData.php b/app/code/Magento/Tax/Setup/InstallData.php index 6869360f82cb4..8e989febac359 100644 --- a/app/code/Magento/Tax/Setup/InstallData.php +++ b/app/code/Magento/Tax/Setup/InstallData.php @@ -159,7 +159,7 @@ private function getRegionId($regionCode) } $regionId = ''; - /** @var $item */ + /** @var \Magento\Directory\Model\Region $item */ $item = $this->regionCollection->getItemByColumnValue('code', $regionCode); if ($item) { $regionId = $item->getId(); From 205dd2632795aa002c241f998df3a4fbfaddbba0 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Mon, 19 Jun 2017 11:41:52 +0300 Subject: [PATCH 223/363] MAGETWO-64499: Error during deploying using " auto_increment_increment = 3" as Mysql option --- app/code/Magento/Tax/Setup/InstallData.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Tax/Setup/InstallData.php b/app/code/Magento/Tax/Setup/InstallData.php index 8e989febac359..a4bcf467487c8 100644 --- a/app/code/Magento/Tax/Setup/InstallData.php +++ b/app/code/Magento/Tax/Setup/InstallData.php @@ -7,7 +7,6 @@ namespace Magento\Tax\Setup; use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; @@ -46,12 +45,10 @@ class InstallData implements InstallDataInterface */ public function __construct( TaxSetupFactory $taxSetupFactory, - CollectionFactory $collectionFactory = null + CollectionFactory $collectionFactory ) { $this->taxSetupFactory = $taxSetupFactory; - $this->regionCollectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get( - \Magento\Directory\Model\ResourceModel\Region\CollectionFactory::class - ); + $this->regionCollectionFactory = $collectionFactory; } /** From 60e11cdaf1bdc70669bad1ed1ebbe07f5885c18c Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Mon, 19 Jun 2017 11:59:22 +0300 Subject: [PATCH 224/363] MAGETWO-61004: Show correct store local date - use timezone configuration --- .../Ui/Component/Form/Element/DataType/Date.php | 16 +++++++--------- .../Magento/Backend/Test/Fixture/Source/Date.php | 13 ++++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index 633340eb82489..ef4ece40cc0c5 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -60,16 +60,14 @@ public function __construct( public function prepare() { $config = $this->getData('config'); - - if (!isset($config['timeOffset'])) { - $config['timeOffset'] = (new \DateTime( - 'now', - new \DateTimeZone( - $this->localeDate->getConfigTimezone() - ) - ))->getOffset(); + if (!isset($config['storeTimeZone'])) { + $storeTimeZone = $this->localeDate->getConfigTimezone(); + $config['storeTimeZone'] = $storeTimeZone; } - + // Set date format pattern by current locale + $localeDateFormat = $this->localeDate->getDateFormat(); + $config['options']['dateFormat'] = $localeDateFormat; + $config['outputDateFormat'] = $localeDateFormat; $this->setData('config', $config); parent::prepare(); diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/Source/Date.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/Source/Date.php index 9bc78a1416ef8..01c6b2bdc8c3e 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/Source/Date.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/Source/Date.php @@ -6,6 +6,7 @@ namespace Magento\Backend\Test\Fixture\Source; +use Magento\Framework\App\ObjectManager; use Magento\Mtf\Fixture\DataSource; /** @@ -13,6 +14,7 @@ * * Data keys: * - pattern (Format a local time/date with delta, e.g. 'm/d/Y -3 days' = current day - 3 days) + * - apply_timezone (true if it is needed to apply timezone) */ class Date extends DataSource { @@ -35,7 +37,16 @@ public function __construct(array $params, $data = []) if (!$timestamp) { throw new \Exception('Invalid date format for "' . $this->params['attribute_code'] . '" field'); } - $date = date(str_replace($delta, '', $data['pattern']), $timestamp); + if (isset($data['apply_timezone']) && $data['apply_timezone'] === true) { + $timezone = ObjectManager::getInstance() + ->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); + $date = new \DateTime(); + $date->setTimestamp($timestamp); + $date->setTimezone(new \DateTimeZone($timezone->getConfigTimezone())); + $date = $date->format(str_replace($delta, '', $data['pattern'])); + } else { + $date = date(str_replace($delta, '', $data['pattern']), $timestamp); + } if (!$date) { $date = date('m/d/Y'); } From 9101a8164efce395505835d7ee1a4b4017536aa6 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Mon, 19 Jun 2017 17:15:47 +0300 Subject: [PATCH 225/363] MAGETWO-69501: Swatch-color was not replaced by image --- .../Magento/Swatches/Helper/DataTest.php | 452 ++++++++++++++++++ .../Magento/Swatches/_files/magento_image.jpg | Bin 0 -> 13864 bytes 2 files changed, 452 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Swatches/_files/magento_image.jpg diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php new file mode 100644 index 0000000000000..d31ed0d76ae3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php @@ -0,0 +1,452 @@ + [ + 'dispatch uri' => 'catalog/product/view/id/', + 'add product id' => true, + ], + 'second page' => [ + 'dispatch uri' => 'catalog/category/view/id/' . self::CATEGORY_ID, + 'add product id' => false, + ], + ]; + } + + /** + * Test correctness of displaying swatch image on frontend. + * + * This test consistently renders two pages containing product, that has image swatch. + * + * @param string $dispatchUri + * @param bool $addProductId + * @return void + * @magentoDbIsolation enabled + * @dataProvider swatchReplacedByImageDataProvider + */ + public function testSwatchReplacedByImage($dispatchUri, $addProductId) + { + if ($addProductId) { + /** @var ProductRepository $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepository::class); + /** @var TypeConfigurable $configurableProduct */ + $configurableProduct = $productRepository->get(self::CONFIGURABLE_PRODUCT_NAME); + $dispatchUri .= $configurableProduct->getId(); + } + $this->dispatch($dispatchUri); + /** @var string $result */ + $result = $this->getResponse()->getBody(); + $this->assertContains('/magento_image.jpg","label":"option 1"', $result); + } + + /** + * Product fixture for testSwatchReplacedByImage. + * + * This fixture creates one configurable product with two simple configurations and link it to category. + * Also it sets an image swatch to first configuration. + * + * @return void + */ + private static function productFixture() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + /** @var $installer CategorySetup */ + $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + /** @var AttributeRepository $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); + /** @var Attribute $attribute */ + $attribute = $attributeRepository->get('catalog_product', self::SWATCH_ATTRIBUTE_NAME); + /* Create simple products per each option value*/ + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + $attributeValues = []; + $attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); + $associatedProductIds = []; + array_shift($options); // two options is enough + // create two configurations (simple products) + $index = 0; + foreach ($options as $option) { + $index++; + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName(self::SIMPLE_PRODUCT_NAME . $index) + ->setSku(self::SIMPLE_PRODUCT_NAME . $index) + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product->setData($attribute->getAttributeCode(), $option->getValue()); + $product = $productRepository->save($product); + // simple products must be in stock + /** @var Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(Item::class); + $stockItem->load($product->getId(), 'product_id'); + if (!$stockItem->getProductId()) { + $stockItem->setProductId($product->getId()); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + } + // add image swatch to first simple product + $fileName = '/m/a/magento_image.jpg'; + $fileLabel = 'Magento image'; + /** @var Product $simpleProduct */ + $simpleProduct = $productRepository->get(self::SIMPLE_PRODUCT_NAME . '1'); + $simpleProduct->setData( + 'media_gallery', + ['images' => ['swatch_image' => ['file' => $fileName, 'label' => $fileLabel]]] + ); + $simpleProduct->setData('swatch_image', $fileName); + $simpleProduct->save(); + // create configurable product + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + /** @var Factory $optionsFactory */ + $optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + $configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], + ]; + $configurableOptions = $optionsFactory->create($configurableAttributesData); + $extensionConfigurableAttributes = $product->getExtensionAttributes(); + $extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); + $extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + $product->setExtensionAttributes($extensionConfigurableAttributes); + $product->setTypeId(TypeConfigurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName(self::CONFIGURABLE_PRODUCT_NAME) + ->setSku(self::CONFIGURABLE_PRODUCT_NAME) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $productRepository->save($product); + // link configurable product to category + /** @var CategoryLinkManagement $categoryLinkManager */ + $categoryLinkManager = Bootstrap::getObjectManager()->get(CategoryLinkManagement::class); + $categoryLinkManager->assignProductToCategories(self::CONFIGURABLE_PRODUCT_NAME, [self::CATEGORY_ID]); + } + + /** + * Product fixture rollback for testSwatchReplacedByImage. + * + * Deletes products created in productFixture. + * + * @return void + */ + private static function productFixtureRollback() + { + /** @var Registry $registry */ + $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $productCollection Collection */ + $productCollection = Bootstrap::getObjectManager()->create(Collection::class); + $productCollection->addFieldToFilter( + [ + ['attribute' => 'sku', 'like' => self::CONFIGURABLE_PRODUCT_NAME], + ['attribute' => 'sku', 'like' => self::SIMPLE_PRODUCT_NAME . '%'], + ] + ); + foreach ($productCollection as $product) { + $product->delete(); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Attribute fixture for testSwatchReplacedByImage. + * + * Creates swatch attribute with two options and assigns it to default attribute set. + * + * @return void + */ + private static function attributeFixture() + { + $eavConfig = Bootstrap::getObjectManager()->get('Magento\Eav\Model\Config'); + /** @var $installer \Magento\Catalog\Setup\CategorySetup */ + $installer = Bootstrap::getObjectManager()->create('Magento\Catalog\Setup\CategorySetup'); + $data = [ + 'is_required' => 0, + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Table', + 'is_visible_on_front' => 1, + 'is_visible_in_advanced_search' => 0, + 'attribute_code' => self::SWATCH_ATTRIBUTE_NAME, + 'backend_type' => 'int', + 'is_searchable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'frontend_label' => self::SWATCH_ATTRIBUTE_NAME, + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_user_defined' => 1, + 'is_global' => 1, + 'update_product_preview_image' => 1, + 'use_product_image_for_swatch' => 1, + 'used_in_product_listing' => 1, + 'frontend_input' => 'select', + 'swatch_input_type' => 'visual', + ]; + $optionsPerAttribute = 2; + $data['swatchvisual']['value'] = array_reduce( + range(1, $optionsPerAttribute), + function ($values, $index) use ($optionsPerAttribute) { + $values['option_' . $index] = '#' + . str_repeat( + dechex(255 * $index / $optionsPerAttribute), + 3 + ); + return $values; + }, + [] + ); + $data['optionvisual']['value'] = array_reduce( + range(1, $optionsPerAttribute), + function ($values, $index) { + $values['option_' . $index] = ['option ' . $index]; + return $values; + }, + [] + ); + $data['options']['option'] = array_reduce( + range(1, $optionsPerAttribute), + function ($values, $index) { + $values[] = [ + 'label' => 'option ' . $index, + 'value' => 'option_' . $index + ]; + return $values; + }, + [] + ); + $options = []; + foreach ($data['options']['option'] as $optionData) { + $options[] = Bootstrap::getObjectManager()->create(AttributeOptionInterface::class) + ->setLabel($optionData['label']) + ->setValue($optionData['value']); + } + $attribute = Bootstrap::getObjectManager()->create( + ProductAttributeInterface::class, + ['data' => $data] + ); + $attribute->setOptions($options); + $attribute->save(); + // Assign attribute to attribute set. + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + $eavConfig->clear(); + } + + /** + * Attribute fixture rollback for testSwatchReplacedByImage. + * + * Deletes attribute created in attributeFixture. + * + * @return void + */ + private static function attributeFixtureRollback() + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var Attribute $attribute */ + $attribute = Bootstrap::getObjectManager() + ->create(Attribute::class); + $attribute->loadByCode(4, self::SWATCH_ATTRIBUTE_NAME); + if ($attribute->getId()) { + $attribute->delete(); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Category fixture for testSwatchReplacedByImage. + * + * Creates one category. + * + * @return void + */ + private static function categoryFixture() + { + $category = Bootstrap::getObjectManager()->create(Category::class); + $category->isObjectNew(true); + $category->setId( + self::CATEGORY_ID + )->setName( + 'Category 1' + )->setParentId( + 2 + )->setPath( + '1/2/' . self::CATEGORY_ID + )->setLevel( + 2 + )->setIsActive( + true + )->save(); + } + + /** + * Category fixture rollback for testSwatchReplacedByImage. + * + * Deletes category created in categoryFixture. + * + * @return void + */ + private static function categoryFixtureRollback() + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $category Category */ + $category = Bootstrap::getObjectManager()->create(Category::class); + $category->load(self::CATEGORY_ID); + if ($category->getId()) { + $category->delete(); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Image fixture for testSwatchReplacedByImage. + * + * Creates media dirs and copies magento_image.jpg to temporary directory. + * + * @return void + */ + private static function imageFixture() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $mediaConfig Config */ + $mediaConfig = $objectManager->get(Config::class); + /** @var $mediaDirectory WriteInterface */ + $mediaDirectory = $objectManager->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $targetDirPath = $mediaConfig->getBaseMediaPath() . str_replace('/', DIRECTORY_SEPARATOR, '/m/a/'); + $targetTmpDirPath = $mediaConfig->getBaseTmpMediaPath() . str_replace('/', DIRECTORY_SEPARATOR, '/m/a/'); + $mediaDirectory->create($targetDirPath); + $mediaDirectory->create($targetTmpDirPath); + $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath + . DIRECTORY_SEPARATOR . 'magento_image.jpg'; + copy(__DIR__ . '/../_files/magento_image.jpg', $targetTmpFilePath); + } + + /** + * Image fixture rollback for testSwatchReplacedByImage. + * + * Deletes directories created in imageFixture. + * + * @return void + */ + private static function imageFixtureRollback() + { + /** @var $config Config */ + $config = Bootstrap::getObjectManager()->get(Config::class); + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = Bootstrap::getObjectManager() + ->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete($config->getBaseMediaPath()); + $mediaDirectory->delete($config->getBaseTmpMediaPath()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/magento_image.jpg b/dev/tests/integration/testsuite/Magento/Swatches/_files/magento_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0816984ee36067f347a0d96bd13459b77df45e4a GIT binary patch literal 13864 zcmb7rbyS;A*KHsWT!K^FAwY3=FYfLXZGqw~6e#ZQ?(SCH-K9WrFYZuUir(;(_x--V z?!A+&%_slcNI%nq0ljp_fO#r5xw5&7$1{MG?hduz$D*y=q5cZ`(UxDyIAP^oN z4hV#U2f@EI5F!Wx+CYeiNXW>@NQkI^HT0K;`JeV80t1AD1A#!Oh={0I=&0yeI4@ot z@Lw44-*1@zy#0swzl`Tz02mPl9tIN#0|vl?VSr$m=Y9Y&00sd45C-tyhVU{tMA#QE z5!3+-{9j&p5D*Rl>Rbk(0-;V!ASM6+vn%>v`v27e9b=2=Pby-WHUAe1WTo}lEug&o7IH2iw6hocbb~*`ra`nFm&=L_B_gpJp zPa!o(;H~=5MyjG+H*31%MP_~WVOMly& z(*u6mk&MuW6TLW$Y%i}r@~;uYCS<~_-X%P`zbK$>=zq*D|8?reZ#jlOU&VCWW#m#y zAT+8~I*k=8fIf){!Z2onCuGd+UvAP_I;oCGc6UAJI5sVy_0^QQPpYa`~=LZ0U?2v2u_u1RF_^RLeL>3}C2a6C$b-s`_BkxbX zkg-X~igr1FaNA`Xx=G_{#8)a_sV*^Pk!iG>e|x+4v4x%@e{j-X;1c)B%v0y~23h8f zq^g@;8o!m+{OZ7&OkZ*o^hXQ;q#Qi*FcwoBFH8iDNFDPnpKnBW>5w4VVIeP--49K> zr6Kw<;&=F?58m);E}ENmdJ4Cg;eAx8*y#Lp6Sf*1%tAa2Re@ofxwF?+k~yyy;ED^M zum8I2?R7Z)OF+o(sPpo0!+TN-*&(2O0qyHmbVO6zD}D0;xBd0kh5=c#@3l9wgtnWg za>s_)U&f>G({@`|vja(>REy;qAZY$_dH7WZFLLShhWd=fRM#6BYuQ<@lBXUkr1pyY zYaiaPM$QDuM)k_4u~CEjD%*R%n_{o_=VZPy`XG@9OZJ@%@n32(>?f%mNP2vZD(y~ZfvHe65w2NvUIyY2h&R?cgN%;tJCXFi3AvLe0m^O%R!jNBi^-egQNE@#_M~` ziw=>XZN!ptt4h6=2f7x~Ak%ag%ZayeyT|1(H;_~7wIi~6D{hC!$oW*)7iy-H5BJfTVX0^BKP4KDZ`M34i#HO5 z|E%Bfa0T+Xv>b1QljZ!y0)URAH}sv4yEZ-@>`YYd-PG$HyL!3L9VOYt8&7zq7 zox&XnPomnzv*zvtBTY%Ie7^nH4*-mtj7%rC=trY}svFpBbo{U5|IZ?WW+OIessX{k z!odUKV1a+rVPId96&MQ>n}&;mnw3)&ft-z!ih~=MR?Nf!nxhb*xeEp!_8HKoZgy=u zFi6MUwclNlZh5Li1;QY*Rf7+yEzQ)}9~H;jF0C18rjm+AtFHY;7&kwNpW|3eXgpLr zM5j+;NQ|7|m@F(PhBYYIRX3UiU-p_Ubp8t-4w4)Ctm&mG#t=TSUBY;aV_)eYo!;vm zoOGRLx3d~M!P&zd=TM0n>|@LCZ5LRH%o?|XO_QAT(|WT(euiz}k&<6L7%CLkq%BHx=1e*gBU$0WY}Q{9ZK!CId}F_2t7p`Sh8 z6f)qVV^DfjQ4p*XT{oCT6US1Mia8NlcT610E;#)L$(ADaL%7yDk5`cX41Q^po??~1 zag107-f)~E>spkgcx%+)+Oi-!jR#gju83nSebvRS8-4vy1%#w(xG|(Xm6;;e^fPR` zdD6A5iASPWv-ZZ6pPJR;+St&Ax@w;<9?+A-wGlI;k+O6H8<*EV6)DuRNM#jA!Jo`)#Ngnc*+-E?& zfE{9k@PE+8Zs@#8>5MMR93x03O48$Fez9F5>cBYUsfYKf*^(tYf@dr^pnUFBStt33Qz7Lj+>tWgMJH87R{{7 zD^!7&*joh=+U_8T74J9uq+|{wa6*EYzsI5_HaELDZRM^7o%ezVG9>=F0)3dQL#tB}v7BUkFqk#t+` zBCZ;;XgUEW`wFgQeC(4>%h)8%n>pdVTuX@NwJqv-LE$_YYp8XpXt(*bXpA&uTzipFGPVuF_dC779jLhwQY**fh zYo{hlV(Eb^QD=FK&*IymGQJm2zz8(`xz@+_TLJ&X4nOpev=G^Yd;02&7548R-dGr8 zv7c;uv+qHd?dEh<(wwi5cMk6&`c?;&la^WHXF3$Tgs@FwvROj!uO37m;EY~5l%798 zQ)ew2$ggV=wQsdoIbo3@YjpVKK&6TYNYe?HO0OkBd4=vx? z_e9$C%!F=)G?Xcsu}VW(E1#mSNJF8lY?+pVCYUZcRN%oNb`&&St+eVNIATA7lsihC zi_uFMF)4vI;!ImsTGcXQTcx}yok&Tvcs`dv&%=~H=F`hCG7rv%^h-XsOV?NOv#mZ1V%y-*j%ms-0=C}z8KL~HW_IM^`xd|kY zsjOK`CBJeOq<&+@sQnC3MePs882h-VwX!%TB$1`PgtEHbSWszyQf2rI7}pQAGp4aUIA(EF;wBDTe}ysV zB3uOK8IUe2yY@xrMt$mns-UXABGUZv!IP7!x-10p6H|>l?YmK3=X!l3x7^s>$h`K- zcz_0W%~9UiRB*VQ!ejtBHK@|6>S<%xCV?{(2Nz|{v7tE=7x#_Ku*mAr?GwfoULf^B zUhZuOq52HZi899%UkKl4)`2qZ>zy;^CnUs>KOMu(cxEUD?YAL+Bx-&q)ww@;G6`}- zGfKg$!cf^&T5%=*Vwq}gLZYN)V_I# z=G6RM`H2}V_&cst@=kZGhL|i)+gfvr<1?Up%-V*Z9)eqBt}ju`W9aA%#(s_Vv^8{I zHM@r<{aK7KM&kFCuo6#q0+Wq8B(97Kbp)3w>GiTjTm%;_!_kZ_a=2F@ULuQCRK=)*sU8 zcgQ;Gehfibw~g0e%=hnaUjITXs3OGuK^&~-W`wmVpF0%FP&OGrNlTlT$0eVQS=?!~ z%i`Jl%B&!%vCey|c*Ld*y#VJx-M101qX{#@hCaxU?_=HL5~0oszxEd}7%zznQ&Yv8 zdd^jK?x{zf%kFFA+*j*@GSfr36)T0wCmLJM(hpokGO-AH`tN2`RZit11%Pa{GHUz; zW^62^exn!T{Mg>Jb*61BuYVc0Q88keq;8i}{-&@va=I}vb{HixFYyDv{}yCir8SoL zM)Gc(ME(P9I3%RhDZK+fd1E!%EQih_9&O3}?@@h*o5Iz_4P~avkp#1Q41B5dUxw$$ zDeG-HPON~$Fx>R_8j?=)Au5xW8XsG`f&$Z&1Ha4Hs$kcR1f8_EEjhZGo71CKVKZr( zYq5o74D&8)rO`|Frm-mgz^fVg4&E@RtRx3#UzwsKT3hfnZliG~(1wEBdY|%-&D(Hc z;2m;Ok}Zu4Pa4`F>^R7b5el`Mu{x8K&aZQRAq1#Pehfk!MT2UnY8gd51L83&VvPuq z7O6f4KFWL_af1LY%IImOmb8;UGrb;N7x%R&GqVV*2pe?7PIV;)7LdLvq%ineSAop& zq`(?81N$%08v>Ln5%;q+OoUEC$4q-5FtE%WDG5@?NZ7ArF^B00^Sy7RfFr55sK(}| zzfTb~jVgVPau;10_n_giN5CwOgRM9--nL|Pu&%{-K`Q>AK5<@gp;#+m=cBh-gnkQB zuA;@9rk*B+*GLX$@bf?|&afMhg1*FD3iV zoHJnho+|ZY-Rw!punl}xY3R$-+*FL z$tb^71}Fsd1ZhP-=jgX$Rj8nLf>J30hVO z7gEAfzU_+C%}!ruV6VX8zJZBFE9;%em~SiI;SvSh+a{dP!@A|eI!)! zYi9+qzI<%_F6`Ev1V>C&Qlv!H&t))9D_*>jAS}7E(yRlz2cyJ;`CwcNH2{&!jV&_b zB904&v}blR2S@wEJ7P6%I4P_%){;2siQFNLj;^0W(8$gW2EYJepp_i>e^;e1H4`{Y zFa?_=`S-Z7-4loINr;Q zMOoEUnvGK+@_ATMY$%8ZhNx%^(RUOdjR3hczl!#XXi`VHcMM)Hy zkEGJAWK>2kDzqw>MEraZu7m&$e#0XYq}T?uIj*cpwR-(I zGRG(tLu1r|_Ys?6_2hyCTkq9tqzO0!^~14GjwINJJzGE3Bpz|Z@M( zVq=3zxfSZ;e>leSn50SqC_5yS-W>3km_7qAG>OqHJ5nj$&rNS_jFly&_|8$`OTwaw zg&`S}%0YsvorCsG*5;hq7Z4|GGV z_D-l`p;uqavcwP5tHZS?-5O;$(o(^n*4^ajkw_4B*}3Z(4K8NfRTsFf?0s$LCnE2g zXx}V^ue@JeK-GxB{_xJPMgYO&Gp@0Ro12>RKYgOkzgDbC4?LU~!Tji?AHv4fq_ARvKbP$- zhR58$>`yR6J@kQ-*L_Lzcd?ZP9bfD1*Npx5Z=E$-xJN#rhgLJYZi(@_xfgvEG!(FF zy(_kPicc%b;9#1I;~TT!^nR|mUGpg5sSVo0)VPSIhv_-IV!Um<&q3G-*HPrcD03B$g4Z89A!85Z^td(n zL=lga-=iHWWGR-Nuct-WA2p9S){&EGm*`+-h};|>#q4KAO1%EKF}O2N>#E1k?RRP( zTVRz*!*4f25kO&Y7q%bcV^qr5@%D+rXMphYL&_woq();aGjMXlE${pl% z+9ej4>0jI*S@j!Qf;W=QlH(@e6ZwQRoYB%nelsl$EDJ!yuc_F&#wIYunoF8{i$)sd zyHAljjH>l{pyHlZ8*xxuBO}O~sdVHhKSc$_%C<$KKf3pvMdVeLAH&dR;WEQiP8I|< zZ&T4BPVRe}O}D@g%r~8lx*gAewKk2_>+_Khl{f7re^95t?8eu$dH>*cnSEoAk6x%| zdyq4RJ@*KHm+W)Uq_w@KPjB!HI9sa_*x!g-(+_u5yVA`O>ry1|DCEu@+w=(+4>Arq zp2M|^*A!BJJFm<$;CjHx;`c36$up6|KC~61BrP%S3%rKtZT-#Hq94Dp?F3n3erw=F zT3vaU_(%PGa{+a zNuL3J@%GPvimH%xQx?U0i9UND5uZY3j+67OI2YLmNh8H3m4jyXLjCBD0B+<2XKD*t z;BWnyOa(Nm_HHvxmGu+A4(xWxxK$N> z?miEX>KLxxry>RlRHKMWv%3U@qH5?<4cgy8V9@Up!EvlncOi+CGd7vcAitE|aqz*bJ>HmiDMf~~gXneaB2uPz?|ldQEfx66-n>KhQNGH99$Rh+PN zz_k^4j6YJb;kETd39R=_yS|A4r6?Xr4>Px?|Hza^iSz3)aS1x;WNr)6|hU zTdGIrn5!(96F|owWz9SszNYiGz&2zQho$~lcijYH0_)+=<7t$m0WytNs|%rt!OEZJ z%ULgo$;fdD+9BZE)YcN;SNQO1Q|NWhHo65eip|a04LIck8(Wa(@yDW~_*lV{j#z2r zc1DkwJ-sFZM7`?y5=(I&L~M@4Wi9W6_GBr2rFvP%T{vnhT7nje#Cw?~q1z|QL<6(Y zP&=NFeZEsa;fhDX&~oDw17j|T$ej0vQDXU zU;0cLy%FFP=vi>l-~RqP-y`U@D@F5HPl?nJ(ZgXDSwc2471HbS*=o}W;-PZq50cwX zQU+c0MDe|$;dGhFb@Y$7GDphDct`c*xTGhwf$`3BSykS5_T(E6rP*XL*~kpN3N7jk z$E296V|wTX%v>j0dLqUNXd_#a1M!n%i#Xm7~u7h;R7oCK+$D4C$3Q+Ek1DeTPDee-)ZiAK#{;&on?(?o~<*sniqQdgr9`;$&) zdS6i7f1z|+>UUuL@Jl}PD?P^GEp^J(6-v;v-UK=X^F-nBSs1WU+qu88{7BWyMsv^U zklSXYGT;j@Hw;o!5OOZ)$W;oT=jFuSsLuN3y216$)Ls2CcMg7&tfzXeC0;APiWpHx zOEu-`Zy{zcn^ZE0Cl%QDiY^ds3lDpA0TqF`x6L1yeO-N{4w+aBDN=!o$9uz3p}%a* zo=Qp_%-ACI;F;ck{wSn^gqWXWL;%h&i6C9wsF-oE(u z*V|*{)C`B_?fUq$%DUe3R97*Gg`Qx(mSSu&c|^-w@*w+&upm->G34>LGqmvs^=~OG zbR*+Qqc$28NeEr+{^X98yC7l+LVBoJ$sVZaBT}j8AhgtYyC?N1>NYlnk%wjVP3u^_ z(S>r+Ee^YH{%Dr5{8%mRDeApq7Mh#8bBe@`2qU>Xl6udtR1S&cwfPO96D!!&YeLj$ zZ>k;5^21=3*wgNy*~8PkQ^p+a00sht9ItFSrfH(d*@D>+O5BnGWWYcfmc zP^c}yX*SwP^v3Tadt3A#hketFwAyg^hJiQP@Kdv0+~O=yEuOGk#4`Vu#PFhs#;eMz zEPhxq|7Qf#{eYL&pd_8cGayI=n%WqlsSOAVUA9B>+dm~40Cd%EiV00=7rwE%wXCA# zJu_P-4wwH{l0_(<0o+X1*9r#Iy;xxI{(H8>A>+=A0wx4K80pk;5=X-+lTsk4Pi2TF z#lG}DayTmS-WK*BG|=gkp&>NW?hp5-S>&w@ ztvpHBr3)fiAckg4%>%J)o`;i^mdT$hCx3xFL?)B%m42Wb-A~pCA2BZf&Gl>z!6g+} zYnWDY2P`7m!ODAb0ILr@+w2cZ+tPz5O4bq*`ju9pcKlJ**P*Z0OA&hHV4LSYEpeTN z3Hm%BHwY%lng#9DN=Zje2rVNV#88|}%JKoQ%X%f?K8|fzNOnaF$(Hia=aWmPr^9*l zm0tdo@XPtYjK#7>r}Px^$tk-IjYPcGa?wiaQB8Cp5kN}#=x|8XP zSRVq6@e3s}cwQl5>=jA!_z1QvCn_mfAG`Z|`{+A8^F6e%x=bMJg>xf!oWBS4#Le;b zNCns*0=U2@Vra$DB_TEByu6|_8K&^%6L&1vSb-AOjIt7c4 zoX8V-()}Y(!LMSD;wjyHm`=z3d4nb7->egyB@RZ7s?E4LMw$i3)wS^15dlZ_78yje z#ED-&B6KnjVcT(iBi8NVf_CHF(Gf(rqmEsotv@?P;G$OOQBf5K*2nRr|v zK#m{V6kCz9syCc5dL^W+hUpt7^ACn9O(_S#6toGeI`r+neS%-$ZNdb;TElV_>7ecu z3REPzpsZDWdTMi7#JDUP(aU$N>Oxr~lc=n3=8Dl`DxeeeBUK|jM}7qJ^r zW0klBGxQ)N#p5a4pN!L_{?D+bRCLQv(zRrb!zg=Z()ZZQKmi}^;_W{MbxSHGol7S#mW7N&KJw1NbDSa zZ*5M}64sG6BhLIsW=zp@>82DFkcuu0?C@ECCjyBThB0OjKw+w9Pq#hga{A-6s`n>|zj{_J!B`aX3X+E?nRG zY^O!#6yxJ@S(|@)2IPqY5NNOvMir|OBzXb!eAua?#S+9Rv`FI$EpUhfTzoF>N2X<- zs}Ez@{T3smyqmQWrZl0j2=dR^ow5fkVSsaR5`7b=|TL*T%y zR@bDCC0@q1)5-W9!byko7lyq7lAg4+E6DHdW=nEq4ZTML-{@b)OX>!_6Hg|KW1_I!VJiwaNpn)EF61}>0bBD`=yP)Iv5n&JBW)*thx&U@^_>NU3sKbi#X!9^)$k z{Gt;E&Y{@Z+z~MW?G`oqV-{|Qqtpt#H zu7?^uESk1L@VeFrp*}eAkM49%)Is8)KFp3v#3z73#w@K7_YoC9WoJG($&Dca=cD>n zQ@$A4H^z`gVy}=10uy1Poh|!K0;k`myP09OV40gQOyPUDT#ZXPdAwYzcdywOoxanC zInjUU5tMDHQ4)!uBUIp(_)cc?-I@h~1!O2J}*L zcCRGD`YQMh>-&B3Kd{f~M1-?fdfjd7gN>c5M?m=nR(K#jS^qToz)u{x=ayF;*{}2@ zN*PNjQ-C*AD8^MabhhliUnA~|=A{{rxTq97QGdywm;*X#1?yiFZmRZmoy#LahJE9+ z^f(%D5!j1wl3tFhQ|hc;q#)VUnxt{V#3oGCA-?m{=q_aeAsEXPyiBV7R9B+J8HR*_ zJZcg|gCtx?G-~VuarPST-Y|~;7P56reYV_}otI)5H zw&#M?)HjxVW=r&+$n{d+hej^jXFxz0s?xl1uodw4HEz`qUbjOAAjp7pUABbZXF4W-NonG zzT>-uMf=AWpWZZ-csdke^>J^yRbMq;5N5!a{wDf*v$M=r^zBBnV^=gqY@^1@O!Yf) z?ftw8FFapZ)wgGWztQ*Z6DNha&w6ZRy#V52@*zo#4mO!R>@_efHryquvx7o#H_wAO zyinNFyL5lJUSc{(STb#vLecEfPlh;A62CAuGyvw(Fy*ulfUQNDa*B|`C*J>i>G&$R zk+!+`#4f=m!p{X&s@bT3b=z~8Vc-Kv)$Jr|o{LCL&q++U(%T!3?!We{E z+B$^|&2C2>$8u5}uN`+R?VQqTPRKkcgANjJBQAi|-mR~nLm_(Z0hLwzMiw?wdeI02CqHtv`~2E+j^ zRuLiJ)hi6cgiH6jG{_=X^C3tns_#ugM%AkWbsyVi!Yd6|;0q5c0|ea0vN%jf$j~?hiP|BG>nJ2L zjqpO+b@#klzd2S!e(}t6koWe>S2~b}Q&<>>QaljBMRdUqfymefn!~UF>Y638BJ3DJ zJx_`=(v^Yb-%nUDWzAxz)+$Mis7huSKH(_w@Sl8Ovqy*9Nx-m9(#@@?!&D(muY&;= zF>pwkoiEyMaqtqlVeZ<)P#EZI9NBTK0 zL`n|Ok!ye+S!ih{kswiBfFd$noHeXzhvL*i0|6$W=F59)5@~o`0qlWJA*!9RxlJCv zA}W$o;eWyhCiG|!1`YrZ4+DVSJPmFCtb9x{9sepf`1V|on>ft;x848^AL+1(Ig11} z8V3w{-F8(`a@=0GoT~QkKOVe0;UG-$tPD(D836!r2!K5`5wrFcZ1%sJ%(mc} zLbf2su4q$jxUZdv0g!gEnw&7G$iny$*>Ld2be>D`TlF8V-(owQ2kEqm`GQ!d8Yfm0 zk5rg5Shg_2TS#kp=_8Sbp`$3cfH*mEpH+&9<85Rgy-h@B1JlaW3XHMbipfIOx+)K% zb zwcE+eOJiK1g5@)r^3 ze%W?@+VZrPbK;MMYccNo^fVix2W6!28xM6Ngz9qmu~x zK=U$*d0ejiBsM18;62GaJ82bhI*jkb+lj^v|;Q8G^n9F5MTNJhJJZ7+E=er z*RUi;`J1=9M;QiCTdV0|#zpn@-pglt=By#V$|ON(pMBsoQNcO(FT#^AC+2`{`<;HL z{*jJocm7Otbi%fq`Mr;GG9~wjY)GY<0r~`3M^$3oQOR^fuQ6Dbt75W>$Ett!Ly16fa#lKIqAa! zkQ2c?O_Znx$e5eh1v*r-JW=(_yN%&Fa8K>Q3AIPOH=W4LS7TyCaIhxwiK#I^4S6yy zqiIy8_<_|}x3;Brno(SK*VVU8p76sTT5y}65!G2U1p5N20$VNhT<6y$aS-RkChEHI zD_GZHVF&M`Llsak`6tV3EP)?U+pjiL0^PAv6^OB7VCg=gWpKAtz>&;$&lA(!mN8M{ zoNdJ>H3VQ9z!-;L;ybbRPoQVt#@2Co01ykOgLweRY$Cx!^G{@?oH{-_@Y};MJ^-{r zzO+MB>Wc4P{2f9$G-+z(L;h3RsS)Kjy0(p`$JLTfBiyjf2tE4p@QPxsDcDa)Mkp>Ha&vI2+?0S39GJGnBErSpUF$%V6-}- zu}o^7j5ENd(q1ux?BVd721_+W+YYB^_N9t^H1V~a#{L!E&j8YFt=~`Y*z_!2DCK8E zD6-5#cwD)7zR5;9;ur4gx(lY@yhqPc>4R(x;-ZYfe3h2NVUKd1`mtAt##TneM0$2H z)W1NuIGtW9sns>%rN59J#IE65AoV_E>UJvBhNC1qRm5(#l?&~#tn4Al_{%>|PCUzt)lbf*~CH>!Dgp@^y&4RZnYy0hHt@_)~b#?JC z#HPOCh`m}v=*L~|GGvdOg`BgHIz>@uRGbX0h;~R)UJni3eDHj53@2(BlI^eVF~-_l zH9I@J#Ai2oQcL@=TFtztKTea@W%LXvHL9(qG~7oON;qjxT~xu2xm8iG=)@>rHikP) zt|EuLG4i`j0yj&|9$w;bkhVe$C+_2=&Ln@+@IC_=PPqna2L%Yf4GBim>g>4%4`p(^ za09q*3&FS6quC*{<@MYAJQdG?zUYwBGuqps7SF*dU2Rm0_ONF_6wR4MJnUfObY9Zr z%!GE5%@rB(-U9Ye%am3!VcYsq8* z$=le>mefhM!7g-{aozlTFQav1o0(VPG8br{fjhl2xhW6Qapl-#Y)|O+je%dISQ5HF zMIGD%hSEl6_&;ymRrRc@@;1Adxl==C;21F_6|q#o6XJSO9%>1E}j4(%Tk~u2>mM#;9V{Lhb}I$r^sx5h=x25m0L#f#{ve#37Aib zyJj&3Yp*=elQcYR^N9BFD=O7%(msJ;1qf;{GkOnqEaPmp-V-V{ddFxbz*Q*u*(1$Q zH>(+_8U@Y$Y8~d#!m{=cMrh?Edz<}0&8rY{Mk&w_DFHnIV+{xXfbG##=5oOuvW2rZ z^mCbPHa9Z4eg}^PQO^KrA?d6*LuZr7XTT_f9til=07m6NFFj};(Iqo(|3MJcn_~;} ztBFOtzecvXw)ibPR$aJyjEJ-P6RS}DIZE#-^Nqa$jabVZH-n@VN!*Wg%i#I8r9V6h zVr?BU<$r{MtjpTq9q}I^{Kl=Xw{l)`K6)%TLf7Z;ry!iGFKUAgudbvgHMVNq@57=0 z6=-${M%;QeE?^n4q)+SRJ=Wj2&F`;UmI7IdtC&AoV)kI}A06**!(E$%V=2hPiAUpZ zA2{l37ykSkcYqXb?iY{nL3jrFPXn1z3;RUBLrIuf!zI;IvJ3%#DK-AsoWVR5S9&W> zLIr<`BNQ{o0Vy=`Q=q*f3grR*5ZNzEkE}TMFahwjzIIeAG V$G&(=*eCo6X5X8P@7_Ky{U49&1Q!4R literal 0 HcmV?d00001 From b6be57af6ea1183b8166d297d097ac28fbb4d94c Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Mon, 19 Jun 2017 18:13:04 +0300 Subject: [PATCH 226/363] MAGETWO-69501: Swatch-color was not replaced by image --- .../integration/testsuite/Magento/Swatches/Helper/DataTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php index d31ed0d76ae3c..760a8bef9c007 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php @@ -6,6 +6,7 @@ namespace Magento\Swatches\Helper; +// @codingStandardsIgnoreStart use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; @@ -30,6 +31,7 @@ use Magento\Framework\Registry; use Magento\TestFramework\TestCase\AbstractController; use Magento\TestFramework\Helper\Bootstrap; +// @codingStandardsIgnoreEnd class DataTest extends AbstractController { @@ -70,7 +72,6 @@ public static function tearDownAfterClass() parent::tearDownAfterClass(); } - /** * Data provider for testSwatchReplacedByImage. * From c2426f7eb23614d24b7f0ca47b6c6c3a2b298c7d Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 20 Jun 2017 10:15:17 +0300 Subject: [PATCH 227/363] MAGETWO-69501: Swatch-color was not replaced by image --- .../testsuite/Magento/Swatches/Helper/DataTest.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php index 760a8bef9c007..1012474345af4 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php @@ -6,7 +6,6 @@ namespace Magento\Swatches\Helper; -// @codingStandardsIgnoreStart use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; @@ -31,8 +30,12 @@ use Magento\Framework\Registry; use Magento\TestFramework\TestCase\AbstractController; use Magento\TestFramework\Helper\Bootstrap; -// @codingStandardsIgnoreEnd +/** + * class DataTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DataTest extends AbstractController { @@ -159,7 +162,9 @@ private static function productFixture() ->setPrice(10) ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + ->setStockData( + ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] + ); $product->setData($attribute->getAttributeCode(), $option->getValue()); $product = $productRepository->save($product); // simple products must be in stock From 4596a3b5d429b810b7a0910f1b2b95e46bfa7103 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 20 Jun 2017 11:15:20 +0300 Subject: [PATCH 228/363] MAGETWO-69501: Swatch-color was not replaced by image --- app/code/Magento/Swatches/Helper/Data.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index aee4fe7f703c1..9093d10b5d2e8 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -352,6 +352,7 @@ private function getSwatchAttributes(Product $product) $this->populateAdditionalDataEavAttribute($attribute); } } + return $attributes; } From 480da7d64147ef71feeff0433583e821a72cc977 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 20 Jun 2017 11:35:06 +0300 Subject: [PATCH 229/363] MAGETWO-69501: Swatch-color was not replaced by image --- .../Magento/Swatches/Helper/DataTest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php index 1012474345af4..7b507ba83113c 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php @@ -24,21 +24,21 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Eav\Model\Entity\Attribute\Source\Table; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Registry; use Magento\TestFramework\TestCase\AbstractController; use Magento\TestFramework\Helper\Bootstrap; - /** - * class DataTest + * Class DataTest * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataTest extends AbstractController { - const SWATCH_ATTRIBUTE_NAME = 'test_swatch_attribute'; const CATEGORY_NAME = 'test_category'; const CATEGORY_ID = 123456; @@ -268,12 +268,12 @@ private static function productFixtureRollback() */ private static function attributeFixture() { - $eavConfig = Bootstrap::getObjectManager()->get('Magento\Eav\Model\Config'); - /** @var $installer \Magento\Catalog\Setup\CategorySetup */ - $installer = Bootstrap::getObjectManager()->create('Magento\Catalog\Setup\CategorySetup'); + $eavConfig = Bootstrap::getObjectManager()->get(EavConfig::class); + /** @var $installer CategorySetup */ + $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); $data = [ 'is_required' => 0, - 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Table', + 'source_model' => Table::class, 'is_visible_on_front' => 1, 'is_visible_in_advanced_search' => 0, 'attribute_code' => self::SWATCH_ATTRIBUTE_NAME, @@ -300,6 +300,7 @@ function ($values, $index) use ($optionsPerAttribute) { dechex(255 * $index / $optionsPerAttribute), 3 ); + return $values; }, [] @@ -308,6 +309,7 @@ function ($values, $index) use ($optionsPerAttribute) { range(1, $optionsPerAttribute), function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; + return $values; }, [] @@ -319,6 +321,7 @@ function ($values, $index) { 'label' => 'option ' . $index, 'value' => 'option_' . $index ]; + return $values; }, [] From 7f7873e66dc752d2d850307c02e952a733540291 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 20 Jun 2017 11:43:47 +0300 Subject: [PATCH 230/363] MAGETWO-69501: Swatch-color was not replaced by image --- .../integration/testsuite/Magento/Swatches/Helper/DataTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php index 7b507ba83113c..f89c5e8a41b62 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Helper/DataTest.php @@ -32,6 +32,7 @@ use Magento\Framework\Registry; use Magento\TestFramework\TestCase\AbstractController; use Magento\TestFramework\Helper\Bootstrap; + /** * Class DataTest * From d9fdcf40a64c66d156aa2ed152744166d49001fc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 20 Jun 2017 14:10:16 +0300 Subject: [PATCH 231/363] MAGETWO-61916: [BP to 2.1][Performance][Customer] replace inefficient algorithm for url rewrites on category save page --- .../Catalog/Controller/Adminhtml/Category.php | 28 +- .../Catalog/Model/ResourceModel/Category.php | 50 ++- .../Unit/Model/ResourceModel/CategoryTest.php | 146 +++++++ .../Category/ChildrenUrlRewriteGenerator.php | 34 +- .../CurrentUrlRewritesRegenerator.php | 123 ++++-- .../Model/Category/Plugin/Storage.php | 35 +- .../CategoryBasedProductRewriteGenerator.php | 61 +++ .../Model/CategoryUrlRewriteGenerator.php | 106 +++-- .../Model/Map/DataCategoryHashMap.php | 104 +++++ .../Map/DataCategoryUrlRewriteDatabaseMap.php | 156 +++++++ .../Map/DataCategoryUsedInProductsHashMap.php | 112 +++++ .../Model/Map/DataProductHashMap.php | 113 +++++ .../Map/DataProductUrlRewriteDatabaseMap.php | 146 +++++++ .../Model/Map/DatabaseMapInterface.php | 38 ++ .../Model/Map/DatabaseMapPool.php | 79 ++++ .../Model/Map/HashMapInterface.php | 43 ++ .../Model/Map/HashMapPool.php | 82 ++++ .../Model/Map/UrlRewriteFinder.php | 146 +++++++ .../Product/CurrentUrlRewritesRegenerator.php | 156 ++++--- .../Model/ProductScopeRewriteGenerator.php | 213 ++++++++++ .../Model/ProductUrlRewriteGenerator.php | 153 +++---- .../Model/ResourceModel/Category/Product.php | 37 ++ .../Model/UrlRewriteBunchReplacer.php | 43 ++ .../Observer/AfterImportDataObserver.php | 43 +- ...ategoryProcessUrlRewriteMovingObserver.php | 26 +- ...ategoryProcessUrlRewriteSavingObserver.php | 91 +++- .../Observer/UrlRewriteHandler.php | 88 +++- .../ChildrenUrlRewriteGeneratorTest.php | 60 ++- .../CurrentUrlRewritesRegeneratorTest.php | 116 +++--- .../Model/Category/Plugin/StorageTest.php | 122 ++++++ ...tegoryBasedProductRewriteGeneratorTest.php | 114 +++++ .../Model/CategoryUrlRewriteGeneratorTest.php | 93 +++-- .../Model/Map/DataCategoryHashMapTest.php | 104 +++++ .../DataCategoryUrlRewriteDatabaseMapTest.php | 125 ++++++ .../DataCategoryUsedInProductsHashMapTest.php | 108 +++++ .../Unit/Model/Map/DataProductHashMapTest.php | 121 ++++++ .../DataProductUrlRewriteDatabaseMapTest.php | 112 +++++ .../Unit/Model/Map/DatabaseMapPoolTest.php | 78 ++++ .../Test/Unit/Model/Map/HashMapPoolTest.php | 89 ++++ .../Unit/Model/Map/UrlRewriteFinderTest.php | 167 ++++++++ .../CurrentUrlRewritesRegeneratorTest.php | 126 +++--- .../ProductScopeRewriteGeneratorTest.php | 194 +++++++++ .../Model/ProductUrlRewriteGeneratorTest.php | 274 ++++-------- .../Model/UrlRewriteBunchReplacerTest.php | 44 ++ .../Observer/AfterImportDataObserverTest.php | 119 +++--- ...oryProcessUrlRewriteMovingObserverTest.php | 321 ++++++++++++++ ...oryProcessUrlRewriteSavingObserverTest.php | 261 ++++++++++++ .../Unit/Observer/UrlRewriteHandlerTest.php | 394 ++++++++++++++++++ app/code/Magento/CatalogUrlRewrite/etc/di.xml | 8 + .../UrlRewrite/Model/MergeDataProvider.php | 56 +++ .../Test/Unit/Model/MergeDataProviderTest.php | 127 ++++++ app/etc/di.xml | 13 + ...oryProcessUrlRewriteSavingObserverTest.php | 186 +++++++++ .../Security/Model/Plugin/AuthSessionTest.php | 20 +- .../Framework/DB/TemporaryTableService.php | 179 ++++++++ .../Test/Unit/TemporaryTableServiceTest.php | 211 ++++++++++ 56 files changed, 5699 insertions(+), 695 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapPool.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapPool.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php create mode 100644 app/code/Magento/UrlRewrite/Model/MergeDataProvider.php create mode 100644 app/code/Magento/UrlRewrite/Test/Unit/Model/MergeDataProviderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserverTest.php create mode 100644 lib/internal/Magento/Framework/DB/TemporaryTableService.php create mode 100644 lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php index 4e376463260d7..464498edcd787 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php @@ -31,16 +31,16 @@ abstract class Category extends \Magento\Backend\App\Action */ protected function _initCategory($getRootInstead = false) { - $categoryId = (int)$this->getRequest()->getParam('id', false); + $categoryId = $this->resolveCategoryId(); $storeId = (int)$this->getRequest()->getParam('store'); - $category = $this->_objectManager->create('Magento\Catalog\Model\Category'); + $category = $this->_objectManager->create(\Magento\Catalog\Model\Category::class); $category->setStoreId($storeId); if ($categoryId) { $category->load($categoryId); if ($storeId) { $rootId = $this->_objectManager->get( - 'Magento\Store\Model\StoreManagerInterface' + \Magento\Store\Model\StoreManagerInterface::class )->getStore( $storeId )->getRootCategoryId(); @@ -55,13 +55,25 @@ protected function _initCategory($getRootInstead = false) } } - $this->_objectManager->get('Magento\Framework\Registry')->register('category', $category); - $this->_objectManager->get('Magento\Framework\Registry')->register('current_category', $category); - $this->_objectManager->get('Magento\Cms\Model\Wysiwyg\Config') + $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category); + $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); + $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class) ->setStoreId($this->getRequest()->getParam('store')); return $category; } + /** + * Resolve Category Id (from get or from post). + * + * @return int + */ + private function resolveCategoryId() + { + $categoryId = (int)$this->getRequest()->getParam('id', false); + + return $categoryId ?: (int)$this->getRequest()->getParam('entity_id', false); + } + /** * Build response for ajax request * @@ -79,7 +91,7 @@ protected function ajaxRequestResponse($category, $resultPage) if (empty($breadcrumbsPath)) { // but if no category, and it is deleted - prepare breadcrumbs from path, saved in session $breadcrumbsPath = $this->_objectManager->get( - 'Magento\Backend\Model\Auth\Session' + \Magento\Backend\Model\Auth\Session::class )->getDeletedPath( true ); @@ -107,7 +119,7 @@ protected function ajaxRequestResponse($category, $resultPage) ['response' => $eventResponse, 'controller' => $this] ); /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->_objectManager->get('Magento\Framework\Controller\Result\Json'); + $resultJson = $this->_objectManager->get(\Magento\Framework\Controller\Result\Json::class); $resultJson->setHeader('Content-type', 'application/json', true); $resultJson->setData($eventResponse->getData()); return $resultJson; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 24b7ffd0881ab..7d3939096fafa 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -33,6 +33,13 @@ class Category extends AbstractResource */ protected $_categoryProductTable; + /** + * Entities where attribute is filled. + * + * @var array[] + */ + private $entitiesWhereAttributesIs; + /** * Id of 'is_active' category attribute * @@ -575,22 +582,29 @@ public function getIsActiveAttributeId() */ public function findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValue) { - $linkField = $this->getLinkField(); - $bind = ['attribute_id' => $attribute->getId(), 'value' => $expectedValue]; - $selectEntities = $this->getConnection()->select()->from( - ['ce' => $this->getTable('catalog_category_entity')], - ['entity_id'] - )->joinLeft( - ['ci' => $attribute->getBackend()->getTable()], - "ci.{$linkField} = ce.{$linkField} AND attribute_id = :attribute_id", - ['value'] - )->where( - 'ci.value = :value' - )->where( - 'ce.entity_id IN (?)', - $entityIdsFilter - ); - return $this->getConnection()->fetchCol($selectEntities, $bind); + $entityIdsFilterHash = md5(serialize($entityIdsFilter)); + + if (!isset($this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue])) { + $linkField = $this->getLinkField(); + $bind = ['attribute_id' => $attribute->getId(), 'value' => $expectedValue]; + $selectEntities = $this->getConnection()->select()->from( + ['ce' => $this->getTable('catalog_category_entity')], + ['entity_id'] + )->joinLeft( + ['ci' => $attribute->getBackend()->getTable()], + "ci.{$linkField} = ce.{$linkField} AND attribute_id = :attribute_id", + ['value'] + )->where( + 'ci.value = :value' + )->where( + 'ce.entity_id IN (?)', + $entityIdsFilter + ); + $this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue] = + $this->getConnection()->fetchCol($selectEntities, $bind); + } + + return $this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue]; } /** @@ -1035,7 +1049,7 @@ private function getEntityManager() { if (null === $this->entityManager) { $this->entityManager = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\EntityManager'); + ->get(\Magento\Framework\EntityManager\EntityManager::class); } return $this->entityManager; } @@ -1047,7 +1061,7 @@ private function getAggregateCount() { if (null === $this->aggregateCount) { $this->aggregateCount = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\ResourceModel\Category\AggregateCount'); + ->get(\Magento\Catalog\Model\ResourceModel\Category\AggregateCount::class); } return $this->aggregateCount; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php new file mode 100644 index 0000000000000..2c91a91042b24 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php @@ -0,0 +1,146 @@ +selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + $this->selectMock->expects($this->at(2))->method('where')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf(); + $this->connectionMock = $this->getMockBuilder(Adapter::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock); + $this->resourceMock = $this->getMockBuilder(ResourceConnection::class)->disableOriginalConstructor()->getMock(); + $this->resourceMock->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); + $this->connectionMock->expects($this->any())->method('getTableName')->willReturn('TableName'); + $this->resourceMock->expects($this->any())->method('getTableName')->willReturn('TableName'); + $this->contextMock = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); + $this->eavConfigMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->entityType = $this->getMockBuilder(Type::class)->disableOriginalConstructor()->getMock(); + $this->eavConfigMock->expects($this->any())->method('getEntityType')->willReturn($this->entityType); + $this->contextMock->expects($this->any())->method('getEavConfig')->willReturn($this->eavConfigMock); + $this->contextMock->expects($this->any())->method('getResource')->willReturn($this->resourceMock); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->factoryMock = $this->getMockBuilder(Factory::class)->disableOriginalConstructor()->getMock(); + $this->managerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); + $this->treeFactoryMock = $this->getMockBuilder(Category\TreeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->category = new Category( + $this->contextMock, + $this->storeManagerMock, + $this->factoryMock, + $this->managerMock, + $this->treeFactoryMock, + $this->collectionFactoryMock, + [] + ); + } + + /** + * @return void + */ + public function testFindWhereAttributeIs() + { + $entityIdsFilter = [1, 2]; + $expectedValue = 123; + $attribute = $this->getMockBuilder(Attribute::class)->disableOriginalConstructor()->getMock(); + $backendModel = $this->getMockBuilder(AbstractBackend::class)->disableOriginalConstructor()->getMock(); + + $attribute->expects($this->any())->method('getBackend')->willReturn($backendModel); + $this->connectionMock->expects($this->once())->method('fetchCol')->willReturn(['result']); + + $result = $this->category->findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValue); + $this->assertEquals(['result'], $result); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php index 349a4772633ed..4824e31965270 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php @@ -7,6 +7,9 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\Framework\App\ObjectManager; class ChildrenUrlRewriteGenerator { @@ -16,16 +19,29 @@ class ChildrenUrlRewriteGenerator /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory */ protected $categoryUrlRewriteGeneratorFactory; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( ChildrenCategoriesProvider $childrenCategoriesProvider, - CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory + CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -33,19 +49,21 @@ public function __construct( * * @param int $storeId * @param \Magento\Catalog\Model\Category $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($storeId, Category $category) + public function generate($storeId, Category $category, $rootCategoryId = null) { - $urls = []; - foreach ($this->childrenCategoriesProvider->getChildren($category) as $childCategory) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { $childCategory->setStoreId($storeId); $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); - $urls = array_merge( - $urls, - $this->categoryUrlRewriteGeneratorFactory->create()->generate($childCategory) + /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ + $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); + $mergeDataProvider->merge( + $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) ); } - return $urls; + return $mergeDataProvider->getData(); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php index d4581744bcb07..4189520806668 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php @@ -5,13 +5,8 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category; -use Magento\Catalog\Model\Category; -use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\UrlRewrite\Model\OptionProvider; -use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; class CurrentUrlRewritesRegenerator { @@ -21,25 +16,64 @@ class CurrentUrlRewritesRegenerator /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory */ protected $urlRewriteFactory; - /** @var UrlFinderInterface */ + /** + * @var \Magento\UrlRewrite\Model\UrlFinderInterface + * @deprecated + */ protected $urlFinder; - /** @var \Magento\Catalog\Model\Category */ + /** + * @var \Magento\Catalog\Model\Category + * @deprecated + */ protected $category; + /** + * Data class for url storage. + * + * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite + */ + private $urlRewritePrototype; + + /** + * Finds specific queried url rewrites identified by specific fields. + * + * @var \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder + */ + private $urlRewriteFinder; + + /** + * Container for new generated url rewrites. + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory - * @param UrlFinderInterface $urlFinder + * @param \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder + * @param \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder|null $urlRewriteFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( - CategoryUrlPathGenerator $categoryUrlPathGenerator, - UrlRewriteFactory $urlRewriteFactory, - UrlFinderInterface $urlFinder + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, + \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory, + \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder, + \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder $urlRewriteFinder = null, + \Magento\UrlRewrite\Model\MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->urlRewriteFactory = $urlRewriteFactory; + $this->urlRewritePrototype = $urlRewriteFactory->create(); $this->urlFinder = $urlFinder; + $this->urlRewriteFinder = $urlRewriteFinder ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class); + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class + ); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -47,70 +81,70 @@ public function __construct( * * @param int $storeId * @param \Magento\Catalog\Model\Category $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($storeId, Category $category) + public function generate($storeId, \Magento\Catalog\Model\Category $category, $rootCategoryId = null) { - $this->category = $category; - - $currentUrlRewrites = $this->urlFinder->findAllByData( - [ - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::ENTITY_ID => $category->getId(), - UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, - ] + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $currentUrlRewrites = $this->urlRewriteFinder->findAllByData( + $category->getEntityId(), + $storeId, + CategoryUrlRewriteGenerator::ENTITY_TYPE, + $rootCategoryId ); - $urlRewrites = []; foreach ($currentUrlRewrites as $rewrite) { - if ($rewrite->getIsAutogenerated()) { - $urlRewrites = array_merge($urlRewrites, $this->generateForAutogenerated($rewrite, $storeId)); - } else { - $urlRewrites = array_merge($urlRewrites, $this->generateForCustom($rewrite, $storeId)); - } + $generated = $rewrite->getIsAutogenerated() + ? $this->generateForAutogenerated($rewrite, $storeId, $category) + : $this->generateForCustom($rewrite, $storeId, $category); + $mergeDataProvider->merge($generated); } - return $urlRewrites; + + return $mergeDataProvider->getData(); } /** * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $url * @param int $storeId + * @param \Magento\Catalog\Model\Category|null $category * @return array */ - protected function generateForAutogenerated($url, $storeId) + protected function generateForAutogenerated($url, $storeId, \Magento\Catalog\Model\Category $category = null) { - $urls = []; - if ($this->category->getData('save_rewrites_history')) { - $targetPath = $this->categoryUrlPathGenerator->getUrlPathWithSuffix($this->category, $storeId); + if ($category->getData('save_rewrites_history')) { + $targetPath = $this->categoryUrlPathGenerator->getUrlPathWithSuffix($category, $storeId); if ($url->getRequestPath() !== $targetPath) { - $urls[$url->getRequestPath() . '_' . $storeId] = $this->urlRewriteFactory->create() - ->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->category->getId()) + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($category->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType(OptionProvider::PERMANENT) ->setStoreId($storeId) ->setIsAutogenerated(0); + + return [$generatedUrl]; } } - return $urls; + return []; } /** * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $url * @param int $storeId - * @return array + * @param \Magento\Catalog\Model\Category|null $category + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForCustom($url, $storeId) + protected function generateForCustom($url, $storeId, \Magento\Catalog\Model\Category $category = null) { - $urls = []; $targetPath = !$url->getRedirectType() ? $url->getTargetPath() - : $this->categoryUrlPathGenerator->getUrlPathWithSuffix($this->category, $storeId); + : $this->categoryUrlPathGenerator->getUrlPathWithSuffix($category, $storeId); if ($url->getRequestPath() !== $targetPath) { - $urls[$url->getRequestPath() . '_' . $storeId] = $this->urlRewriteFactory->create() - ->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->category->getId()) + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($category->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType($url->getRedirectType()) @@ -118,7 +152,10 @@ protected function generateForCustom($url, $storeId) ->setDescription($url->getDescription()) ->setIsAutogenerated(0) ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; } - return $urls; + + return []; } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php index b02e994d66a2a..6da36ba733319 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php @@ -5,30 +5,38 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category\Plugin; -use Magento\CatalogUrlRewrite\Model\Category\ProductFactory; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\UrlRewrite\Model\StorageInterface; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; class Storage { - /** @var UrlFinderInterface */ - protected $urlFinder; + /** + * Url Finder Interface. + * + * @var UrlFinderInterface + */ + private $urlFinder; - /** @var ProductFactory */ - protected $productFactory; + /** + * Product resource model. + * + * @var Product + */ + private $productResource; /** * @param UrlFinderInterface $urlFinder - * @param ProductFactory $productFactory + * @param Product $productResource */ public function __construct( UrlFinderInterface $urlFinder, - ProductFactory $productFactory + Product $productResource ) { $this->urlFinder = $urlFinder; - $this->productFactory = $productFactory; + $this->productResource = $productResource; } /** @@ -51,7 +59,7 @@ public function aroundReplace(StorageInterface $object, \Closure $proceed, array ]; } if ($toSave) { - $this->productFactory->create()->getResource()->saveMultiple($toSave); + $this->productResource->saveMultiple($toSave); } } @@ -63,14 +71,7 @@ public function aroundReplace(StorageInterface $object, \Closure $proceed, array */ public function beforeDeleteByData(StorageInterface $object, array $data) { - $toRemove = []; - $records = $this->urlFinder->findAllByData($data); - foreach ($records as $record) { - $toRemove[] = $record->getUrlRewriteId(); - } - if ($toRemove) { - $this->productFactory->create()->getResource()->removeMultiple($toRemove); - } + $this->productResource->removeMultipleByProductCategory($data); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php new file mode 100644 index 0000000000000..5f52c7a158ad3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php @@ -0,0 +1,61 @@ +productScopeRewriteGenerator = $productScopeRewriteGenerator; + } + + /** + * Generates product url rewrites based on category + * + * @param Product $product + * @param Category $category + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function generate(Product $product, Category $category, $rootCategoryId = null) + { + if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) { + return []; + } + + $storeId = $product->getStoreId(); + + $urls = $this->productScopeRewriteGenerator->isGlobalScope($storeId) + ? $this->productScopeRewriteGenerator->generateForGlobalScope([$category], $product, $rootCategoryId) + : $this->productScopeRewriteGenerator->generateForSpecificStoreView( + $storeId, + [$category], + $product, + $rootCategoryId + ); + + return $urls; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index 5748b69ea3ce4..34063c54b02ea 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -12,6 +12,8 @@ use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Store\Model\Store; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; class CategoryUrlRewriteGenerator { @@ -21,7 +23,10 @@ class CategoryUrlRewriteGenerator /** @var StoreViewService */ protected $storeViewService; - /** @var \Magento\Catalog\Model\Category */ + /** + * @var \Magento\Catalog\Model\Category + * @deprecated + */ protected $category; /** @var \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator */ @@ -38,72 +43,97 @@ class CategoryUrlRewriteGenerator */ protected $overrideStoreUrls; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator * @param \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator $currentUrlRewritesRegenerator * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator $childrenUrlRewriteGenerator * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator, CurrentUrlRewritesRegenerator $currentUrlRewritesRegenerator, ChildrenUrlRewriteGenerator $childrenUrlRewriteGenerator, StoreViewService $storeViewService, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->storeViewService = $storeViewService; $this->canonicalUrlRewriteGenerator = $canonicalUrlRewriteGenerator; $this->childrenUrlRewriteGenerator = $childrenUrlRewriteGenerator; $this->currentUrlRewritesRegenerator = $currentUrlRewritesRegenerator; $this->categoryRepository = $categoryRepository; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** - * {@inheritdoc} + * @param \Magento\Catalog\Model\Category $category + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($category, $overrideStoreUrls = false) + public function generate($category, $overrideStoreUrls = false, $rootCategoryId = null) { - $this->category = $category; - $this->overrideStoreUrls = $overrideStoreUrls; + if ($rootCategoryId === null) { + $rootCategoryId = $category->getEntityId(); + } - $storeId = $this->category->getStoreId(); + $storeId = $category->getStoreId(); $urls = $this->isGlobalScope($storeId) - ? $this->generateForGlobalScope() - : $this->generateForSpecificStoreView($storeId); + ? $this->generateForGlobalScope($category, $overrideStoreUrls, $rootCategoryId) + : $this->generateForSpecificStoreView($storeId, $category, $rootCategoryId); - $this->category = null; return $urls; } /** - * Generate list of urls for global scope + * Generate list of urls for global scope. * + * @param \Magento\Catalog\Model\Category|null $category + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForGlobalScope() - { - $urls = []; - $categoryId = $this->category->getId(); - foreach ($this->category->getStoreIds() as $storeId) { + protected function generateForGlobalScope( + Category $category = null, + $overrideStoreUrls = false, + $rootCategoryId = null + ) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $categoryId = $category->getId(); + foreach ($category->getStoreIds() as $storeId) { if (!$this->isGlobalScope($storeId) - && $this->isOverrideUrlsForStore($storeId, $categoryId) + && $this->isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls) ) { - $this->updateCategoryUrlForStore($storeId); - $urls = array_merge($urls, $this->generateForSpecificStoreView($storeId)); + $this->updateCategoryUrlForStore($storeId, $category); + $mergeDataProvider->merge($this->generateForSpecificStoreView($storeId, $category, $rootCategoryId)); } } - return $urls; + $result = $mergeDataProvider->getData(); + + return $result; } /** * @param int $storeId * @param int $categoryId + * @param bool $overrideStoreUrls * @return bool */ - protected function isOverrideUrlsForStore($storeId, $categoryId) + protected function isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls = false) { - return $this->overrideStoreUrls || !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + return $overrideStoreUrls || !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( $storeId, $categoryId, Category::ENTITY @@ -114,12 +144,18 @@ protected function isOverrideUrlsForStore($storeId, $categoryId) * Override url key and url path for category in specific Store * * @param int $storeId + * @param \Magento\Catalog\Model\Category|null $category * @return void */ - protected function updateCategoryUrlForStore($storeId) + protected function updateCategoryUrlForStore($storeId, Category $category = null) { - $category = $this->categoryRepository->get($this->category->getId(), $storeId); - $this->category->addData(['url_key' => $category->getUrlKey(), 'url_path' => $category->getUrlPath()]); + $categoryFromRepository = $this->categoryRepository->get($category->getId(), $storeId); + $category->addData( + [ + 'url_key' => $categoryFromRepository->getUrlKey(), + 'url_path' => $categoryFromRepository->getUrlPath() + ] + ); } /** @@ -137,15 +173,23 @@ protected function isGlobalScope($storeId) * Generate list of urls per store * * @param string $storeId + * @param \Magento\Catalog\Model\Category|null $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForSpecificStoreView($storeId) + protected function generateForSpecificStoreView($storeId, Category $category = null, $rootCategoryId = null) { - $urls = array_merge( - $this->canonicalUrlRewriteGenerator->generate($storeId, $this->category), - $this->childrenUrlRewriteGenerator->generate($storeId, $this->category), - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $mergeDataProvider->merge( + $this->canonicalUrlRewriteGenerator->generate($storeId, $category) ); - return $urls; + $mergeDataProvider->merge( + $this->childrenUrlRewriteGenerator->generate($storeId, $category, $rootCategoryId) + ); + $mergeDataProvider->merge( + $this->currentUrlRewritesRegenerator->generate($storeId, $category, $rootCategoryId) + ); + + return $mergeDataProvider->getData(); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php new file mode 100644 index 0000000000000..5a05414e084c9 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php @@ -0,0 +1,104 @@ +categoryRepository = $categoryRepository; + $this->categoryResourceFactory = $categoryResourceFactory; + } + + /** + * Returns an array of categories ids that includes category identified by $categoryId and all its subcategories. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $category = $this->categoryRepository->get($categoryId); + $this->hashMap[$categoryId] = $this->getAllCategoryChildrenIds($category); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * Queries the database for sub-categories ids from a category. + * + * @param CategoryInterface $category + * @return int[] + */ + private function getAllCategoryChildrenIds(CategoryInterface $category) + { + $categoryResource = $this->categoryResourceFactory->create(); + $connection = $categoryResource->getConnection(); + $select = $connection->select() + ->from($categoryResource->getEntityTable(), 'entity_id') + ->where($connection->quoteIdentifier('path') . ' LIKE :c_path'); + $bind = ['c_path' => $category->getPath() . '%']; + + return $connection->fetchCol($select, $bind); + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php new file mode 100644 index 0000000000000..861696e23de02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php @@ -0,0 +1,156 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + $this->temporaryTableService = $temporaryTableService; + } + + /** + * Generates data from categoryId and stores it into a temporary table. + * + * @param int $categoryId + * @return void + */ + private function generateTableAdapter($categoryId) + { + if (!isset($this->createdTableAdapters[$categoryId])) { + $this->createdTableAdapters[$categoryId] = $this->generateData($categoryId); + } + } + + /** + * Queries the database for all category url rewrites that are affected by the category identified by $categoryId. + * It returns the name of the temporary table where the resulting data is stored. + * + * @param int $categoryId + * @return string + */ + private function generateData($categoryId) + { + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from( + ['e' => $this->connection->getTableName('url_rewrite')], + ['e.*', 'hash_key' => new \Zend_Db_Expr( + "CONCAT(e.store_id,'" . MergeDataProvider::SEPARATOR . "', e.entity_id)" + ) + ] + ) + ->where('entity_type = ?', $this->entityType) + ->where( + $urlRewritesConnection->prepareSqlCondition( + 'entity_id', + [ + 'in' => array_merge( + $this->hashMapPool->getDataMap(DataCategoryUsedInProductsHashMap::class, $categoryId) + ->getAllData($categoryId), + $this->hashMapPool->getDataMap(DataCategoryHashMap::class, $categoryId) + ->getAllData($categoryId) + ) + ] + ) + ); + $mapName = $this->temporaryTableService->createFromSelect( + $select, + $this->connection->getConnection(), + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ); + + return $mapName; + } + + /** + * {@inheritdoc} + */ + public function destroyTableAdapter($categoryId) + { + $this->hashMapPool->resetMap(DataCategoryUsedInProductsHashMap::class, $categoryId); + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + if (isset($this->createdTableAdapters[$categoryId])) { + $this->temporaryTableService->dropTable($this->createdTableAdapters[$categoryId]); + unset($this->createdTableAdapters[$categoryId]); + } + } + + /** + * Gets data by criteria from a map identified by a category Id. + * + * @param int $categoryId + * @param string $key + * @return array + */ + public function getData($categoryId, $key) + { + $this->generateTableAdapter($categoryId); + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select()->from(['e' => $this->createdTableAdapters[$categoryId]]); + if (strlen($key) > 0) { + $select->where('hash_key = ?', $key); + } + + return $urlRewritesConnection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php new file mode 100644 index 0000000000000..4e2a0c4bfc386 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php @@ -0,0 +1,112 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + } + + /** + * Returns an array of product ids for all DataProductHashMap list, + * that occur in other categories not part of DataCategoryHashMap list. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $productsLinkConnection = $this->connection->getConnection(); + $select = $productsLinkConnection->select() + ->from($this->connection->getTableName('catalog_category_product'), ['category_id']) + ->where( + $productsLinkConnection->prepareSqlCondition( + 'product_id', + [ + 'in' => $this->hashMapPool->getDataMap( + DataProductHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + ) + ->where( + $productsLinkConnection->prepareSqlCondition( + 'category_id', + [ + 'nin' => $this->hashMapPool->getDataMap( + DataCategoryHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + )->group('category_id'); + + $this->hashMap[$categoryId] = $productsLinkConnection->fetchCol($select); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + $this->hashMapPool->resetMap(DataProductHashMap::class, $categoryId); + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php new file mode 100644 index 0000000000000..67712ff23b3cb --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php @@ -0,0 +1,113 @@ +collectionFactory = $collectionFactory; + $this->hashMapPool = $hashMapPool; + $this->connection = $connection; + } + + /** + * Returns an array of ids of all visible products and assigned to a category and all its subcategories. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $productsCollection = $this->collectionFactory->create(); + $productsCollection->getSelect() + ->joinInner( + ['cp' => $this->connection->getTableName('catalog_category_product')], + 'cp.product_id = e.entity_id', + [] + ) + ->where( + $productsCollection->getConnection()->prepareSqlCondition( + 'cp.category_id', + [ + 'in' => $this->hashMapPool->getDataMap( + DataCategoryHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + )->group('e.entity_id'); + $this->hashMap[$categoryId] = $productsCollection->getAllIds(); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php new file mode 100644 index 0000000000000..1ae302c6873b1 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php @@ -0,0 +1,146 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + $this->temporaryTableService = $temporaryTableService; + } + + /** + * Generates data from categoryId and stores it into a temporary table. + * + * @param int $categoryId + * @return void + */ + private function generateTableAdapter($categoryId) + { + if (!isset($this->createdTableAdapters[$categoryId])) { + $this->createdTableAdapters[$categoryId] = $this->generateData($categoryId); + } + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $this->generateTableAdapter($categoryId); + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from(['e' => $this->createdTableAdapters[$categoryId]]) + ->where('hash_key = ?', $key); + + return $urlRewritesConnection->fetchAll($select); + } + + /** + * Queries the database for all category url rewrites that are affected by the category identified by $categoryId. + * It returns the name of the temporary table where the resulting data is stored. + * + * @param int $categoryId + * @return string + */ + private function generateData($categoryId) + { + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from( + ['e' => $this->connection->getTableName('url_rewrite')], + ['e.*', 'hash_key' => new \Zend_Db_Expr( + "CONCAT(e.store_id,'" . MergeDataProvider::SEPARATOR . "', e.entity_id)" + ) + ] + ) + ->where('entity_type = ?', $this->entityType) + ->where( + $urlRewritesConnection->prepareSqlCondition( + 'entity_id', + [ + 'in' => $this->hashMapPool->getDataMap(DataProductHashMap::class, $categoryId) + ->getAllData($categoryId) + ] + ) + ); + $mapName = $this->temporaryTableService->createFromSelect( + $select, + $this->connection->getConnection(), + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ); + + return $mapName; + } + + /** + * {@inheritdoc} + */ + public function destroyTableAdapter($categoryId) + { + $this->hashMapPool->resetMap(DataProductHashMap::class, $categoryId); + if (isset($this->createdTableAdapters[$categoryId])) { + $this->temporaryTableService->dropTable($this->createdTableAdapters[$categoryId]); + unset($this->createdTableAdapters[$categoryId]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php new file mode 100644 index 0000000000000..1365c81435398 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php @@ -0,0 +1,38 @@ +objectManager = $objectManager; + } + + /** + * Gets a map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return DatabaseMapInterface + */ + public function getDataMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (!isset($this->dataArray[$key])) { + $instance = $this->objectManager->create( + $instanceName, + [ + 'category' => $categoryId + ] + ); + if (!$instance instanceof DatabaseMapInterface) { + throw new \InvalidArgumentException( + $instanceName . ' does not implement interface ' . DatabaseMapInterface::class + ); + } + $this->dataArray[$key] = $instance; + } + + return $this->dataArray[$key]; + } + + /** + * Resets a database map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return void + */ + public function resetMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (isset($this->dataArray[$key])) { + $this->dataArray[$key]->destroyTableAdapter($categoryId); + unset($this->dataArray[$key]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php new file mode 100644 index 0000000000000..bda0abfba60ef --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php @@ -0,0 +1,43 @@ +objectManager = $objectManager; + } + + /** + * Gets a map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return HashMapInterface + * @throws \Exception + */ + public function getDataMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (!isset($this->dataArray[$key])) { + $instance = $this->objectManager->create( + $instanceName, + [ + 'category' => $categoryId + ] + ); + if (!$instance instanceof HashMapInterface) { + throw new \InvalidArgumentException( + $instanceName . ' does not implement interface ' . HashMapInterface::class + ); + } + $this->dataArray[$key] = $instance; + } + + return $this->dataArray[$key]; + } + + /** + * Resets data in a hash map by instance name and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return void + */ + public function resetMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (isset($this->dataArray[$key])) { + $this->dataArray[$key]->resetData($categoryId); + unset($this->dataArray[$key]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php new file mode 100644 index 0000000000000..61670c15ed23e --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php @@ -0,0 +1,146 @@ +databaseMapPool = $databaseMapPool; + $this->urlFinder = $urlFinder; + $this->urlRewriteClassNames = $urlRewriteClassNames; + $this->urlRewritePrototype = $urlRewriteFactory->create(); + } + + /** + * Retrieves existing url rewrites filtered by identifiers from prebuild database maps. + * This method will fall-back to by using UrlFinderInterface when map type is not found in configured list. + * + * @param int $entityId + * @param int $storeId + * @param string $entityType + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function findAllByData($entityId, $storeId, $entityType, $rootCategoryId = null) + { + if ($rootCategoryId + && is_numeric($entityId) + && is_numeric($storeId) + && is_string($entityType) + && isset($this->urlRewriteClassNames[$entityType]) + ) { + $map = $this->databaseMapPool->getDataMap($this->urlRewriteClassNames[$entityType], $rootCategoryId); + if ($map) { + $key = $storeId . '_' . $entityId; + + return $this->arrayToUrlRewriteObject($map->getData($rootCategoryId, $key)); + } + } + + return $this->urlFinder->findAllByData( + [ + UrlRewrite::STORE_ID => $storeId, + UrlRewrite::ENTITY_ID => $entityId, + UrlRewrite::ENTITY_TYPE => $entityType + ] + ); + } + + /** + * Transfers an array values to url rewrite object values. + * + * @param array $data + * @return UrlRewrite[] + */ + private function arrayToUrlRewriteObject(array $data) + { + foreach ($data as $key => $array) { + $data[$key] = $this->createUrlRewrite($array); + } + + return $data; + } + + /** + * Creates url rewrite object and sets $data to its properties by key->value. + * + * @param array $data + * @return UrlRewrite + */ + private function createUrlRewrite(array $data) + { + $dataObject = clone $this->urlRewritePrototype; + $dataObject->setUrlRewriteId($data['url_rewrite_id']); + $dataObject->setEntityType($data['entity_type']); + $dataObject->setEntityId($data['entity_id']); + $dataObject->setRequestPath($data['request_path']); + $dataObject->setTargetPath($data['target_path']); + $dataObject->setRedirectType($data['redirect_type']); + $dataObject->setStoreId($data['store_id']); + $dataObject->setDescription($data['description']); + $dataObject->setIsAutogenerated($data['is_autogenerated']); + $dataObject->setMetadata($data['metadata']); + + return $dataObject; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php index 5f1f01f7b08a8..72b03df45f70f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php @@ -14,20 +14,31 @@ use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -use Magento\Store\Model\StoreManagerInterface; +use Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CurrentUrlRewritesRegenerator { - /** @var Product */ + /** + * @var Product + * @deprecated + */ protected $product; - /** @var ObjectRegistry */ + /** + * @var ObjectRegistry + * @deprecated + */ protected $productCategories; - /** @var UrlFinderInterface */ + /** + * @var UrlFinderInterface + * @deprecated + */ protected $urlFinder; /** @var ProductUrlPathGenerator */ @@ -36,19 +47,49 @@ class CurrentUrlRewritesRegenerator /** @var UrlRewriteFactory */ protected $urlRewriteFactory; + /** + * Data class for url storage. + * + * @var UrlRewrite + */ + private $urlRewritePrototype; + + /** + * Finds specific queried url rewrites identified by specific fields. + * @var UrlRewriteFinder + */ + private $urlRewriteFinder; + + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param UrlFinderInterface $urlFinder * @param ProductUrlPathGenerator $productUrlPathGenerator * @param UrlRewriteFactory $urlRewriteFactory + * @param UrlRewriteFinder|null $urlRewriteFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( UrlFinderInterface $urlFinder, ProductUrlPathGenerator $productUrlPathGenerator, - UrlRewriteFactory $urlRewriteFactory + UrlRewriteFactory $urlRewriteFactory, + UrlRewriteFinder $urlRewriteFinder = null, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->urlFinder = $urlFinder; $this->productUrlPathGenerator = $productUrlPathGenerator; $this->urlRewriteFactory = $urlRewriteFactory; + $this->urlRewritePrototype = $urlRewriteFactory->create(); + $this->urlRewriteFinder = $urlRewriteFinder ?: ObjectManager::getInstance()->get(UrlRewriteFinder::class); + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -57,106 +98,107 @@ public function __construct( * @param int $storeId * @param Product $product * @param ObjectRegistry $productCategories + * @param int|null $rootCategoryId * @return UrlRewrite[] */ - public function generate($storeId, Product $product, ObjectRegistry $productCategories) + public function generate($storeId, Product $product, ObjectRegistry $productCategories, $rootCategoryId = null) { - $this->product = $product; - $this->productCategories = $productCategories; - - $currentUrlRewrites = $this->urlFinder->findAllByData( - [ - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::ENTITY_ID => $this->product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ] + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $currentUrlRewrites = $this->urlRewriteFinder->findAllByData( + $product->getEntityId(), + $storeId, + ProductUrlRewriteGenerator::ENTITY_TYPE, + $rootCategoryId ); - $urlRewrites = []; foreach ($currentUrlRewrites as $currentUrlRewrite) { - $category = $this->retrieveCategoryFromMetadata($currentUrlRewrite); + $category = $this->retrieveCategoryFromMetadata($currentUrlRewrite, $productCategories); if ($category === false) { continue; } - $url = $currentUrlRewrite->getIsAutogenerated() - ? $this->generateForAutogenerated($currentUrlRewrite, $storeId, $category) - : $this->generateForCustom($currentUrlRewrite, $storeId, $category); - $urlRewrites = array_merge($urlRewrites, $url); + $generated = $currentUrlRewrite->getIsAutogenerated() + ? $this->generateForAutogenerated($currentUrlRewrite, $storeId, $category, $product) + : $this->generateForCustom($currentUrlRewrite, $storeId, $category, $product); + + $mergeDataProvider->merge($generated); } - $this->product = null; - $this->productCategories = null; - return $urlRewrites; + return $mergeDataProvider->getData(); } /** * @param UrlRewrite $url * @param int $storeId * @param Category|null $category + * @param Product|null $product * @return array */ - protected function generateForAutogenerated($url, $storeId, $category) + protected function generateForAutogenerated($url, $storeId, $category, $product = null) { - if (!$this->product->getData('save_rewrites_history')) { - return []; - } - $targetPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($this->product, $storeId, $category); - if ($url->getRequestPath() === $targetPath) { - return []; + if ($product->getData('save_rewrites_history')) { + $targetPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); + if ($url->getRequestPath() !== $targetPath) { + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getEntityId()) + ->setRequestPath($url->getRequestPath()) + ->setTargetPath($targetPath) + ->setRedirectType(OptionProvider::PERMANENT) + ->setStoreId($storeId) + ->setDescription($url->getDescription()) + ->setIsAutogenerated(0) + ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; + } } - return [ - $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->product->getId()) - ->setRequestPath($url->getRequestPath()) - ->setTargetPath($targetPath) - ->setRedirectType(OptionProvider::PERMANENT) - ->setStoreId($storeId) - ->setDescription($url->getDescription()) - ->setIsAutogenerated(0) - ->setMetadata($url->getMetadata()) - ]; + + return []; } /** * @param UrlRewrite $url * @param int $storeId * @param Category|null $category - * @return array + * @param Product|null $product + * @return UrlRewrite[] */ - protected function generateForCustom($url, $storeId, $category) + protected function generateForCustom($url, $storeId, $category, $product = null) { $targetPath = $url->getRedirectType() - ? $this->productUrlPathGenerator->getUrlPathWithSuffix($this->product, $storeId, $category) + ? $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category) : $url->getTargetPath(); - if ($url->getRequestPath() === $targetPath) { - return []; - } - return [ - $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->product->getId()) + if ($url->getRequestPath() !== $targetPath) { + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType($url->getRedirectType()) ->setStoreId($storeId) ->setDescription($url->getDescription()) ->setIsAutogenerated(0) - ->setMetadata($url->getMetadata()) - ]; + ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; + } + + return []; } /** * @param UrlRewrite $url + * @param ObjectRegistry|null $productCategories * @return Category|null|bool */ - protected function retrieveCategoryFromMetadata($url) + protected function retrieveCategoryFromMetadata($url, ObjectRegistry $productCategories = null) { $metadata = $url->getMetadata(); if (isset($metadata['category_id'])) { - $category = $this->productCategories->get($metadata['category_id']); + $category = $productCategories->get($metadata['category_id']); return $category === null ? false : $category; } + return null; } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php new file mode 100644 index 0000000000000..40753a16f69a3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -0,0 +1,213 @@ +storeViewService = $storeViewService; + $this->storeManager = $storeManager; + $this->objectRegistryFactory = $objectRegistryFactory; + $this->canonicalUrlRewriteGenerator = $canonicalUrlRewriteGenerator; + $this->categoriesUrlRewriteGenerator = $categoriesUrlRewriteGenerator; + $this->currentUrlRewritesRegenerator = $currentUrlRewritesRegenerator; + $this->anchorUrlRewriteGenerator = $anchorUrlRewriteGenerator; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + } + + /** + * Check is global scope. + * + * @param int|null $storeId + * @return bool + */ + public function isGlobalScope($storeId) + { + return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + } + + /** + * Generate url rewrites for global scope. + * + * @param \Magento\Framework\Data\Collection|\Magento\Catalog\Model\Category[] $productCategories + * @param Product $product + * @param int|null $rootCategoryId + * @return array + */ + public function generateForGlobalScope($productCategories, Product $product, $rootCategoryId = null) + { + $productId = $product->getEntityId(); + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + + foreach ($product->getStoreIds() as $id) { + if (!$this->isGlobalScope($id) && + !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $id, + $productId, + Product::ENTITY + )) { + $mergeDataProvider->merge( + $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId) + ); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Generate list of urls for specific store view. + * + * @param int $storeId + * @param \Magento\Framework\Data\Collection|Category[] $productCategories + * @param \Magento\Catalog\Model\Product $product + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = null) + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $categories = []; + foreach ($productCategories as $category) { + if ($this->isCategoryProperForGenerating($category, $storeId)) { + $categories[] = $category; + } + } + $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); + + $mergeDataProvider->merge( + $this->canonicalUrlRewriteGenerator->generate($storeId, $product) + ); + $mergeDataProvider->merge( + $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) + ); + $mergeDataProvider->merge( + $this->currentUrlRewritesRegenerator->generate( + $storeId, + $product, + $productCategories, + $rootCategoryId + ) + ); + $mergeDataProvider->merge( + $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) + ); + + return $mergeDataProvider->getData(); + } + + /** + * Check possibility for url rewrite generation. + * + * @param \Magento\Catalog\Model\Category $category + * @param int $storeId + * @return bool + */ + public function isCategoryProperForGenerating(Category $category, $storeId) + { + if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + list(, $rootCategoryId) = $category->getParentIds(); + return $rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId(); + } + + return false; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php index 8519492d76346..8522c854a0a91 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php @@ -9,9 +9,8 @@ use Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator; -use Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Product\Visibility; /** @@ -26,32 +25,60 @@ class ProductUrlRewriteGenerator */ const ENTITY_TYPE = 'product'; - /** @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService */ + /** + * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService + * @deprecated + */ protected $storeViewService; - /** @var \Magento\Catalog\Model\Product */ + /** + * @var \Magento\Catalog\Model\Product + * @deprecated + */ protected $product; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator + * @deprecated + */ protected $currentUrlRewritesRegenerator; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator + * @deprecated + */ protected $categoriesUrlRewriteGenerator; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator + * @deprecated + */ protected $canonicalUrlRewriteGenerator; - /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory */ + /** + * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory + * @deprecated + */ protected $objectRegistryFactory; - /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry */ + /** + * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry + * @deprecated + */ protected $productCategories; - /** @var \Magento\Store\Model\StoreManagerInterface */ + /** + * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated + */ protected $storeManager; - /** @var AnchorUrlRewriteGenerator */ - private $anchorUrlRewriteGenerator; + /** + * Generates url rewrites for different scopes. + * + * @var ProductScopeRewriteGenerator + */ + private $productScopeRewriteGenerator; /** * @param \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator @@ -78,123 +105,105 @@ public function __construct( } /** - * @return AnchorUrlRewriteGenerator + * Retrieve Delegator for generation rewrites in different scopes. * * @deprecated + * @return ProductScopeRewriteGenerator|mixed */ - private function getAnchorUrlRewriteGenerator() + private function getProductScopeRewriteGenerator() { - if ($this->anchorUrlRewriteGenerator === null) { - $this->anchorUrlRewriteGenerator = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator'); + if (!$this->productScopeRewriteGenerator) { + $this->productScopeRewriteGenerator = ObjectManager::getInstance() + ->get(ProductScopeRewriteGenerator::class); } - return $this->anchorUrlRewriteGenerator; + + return $this->productScopeRewriteGenerator; } /** - * Generate product url rewrites + * Generate product url rewrites. * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate(Product $product) + public function generate(Product $product, $rootCategoryId = null) { if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) { return []; } - $this->product = $product; - $storeId = $this->product->getStoreId(); + $storeId = $product->getStoreId(); $productCategories = $product->getCategoryCollection() ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); $urls = $this->isGlobalScope($storeId) - ? $this->generateForGlobalScope($productCategories) - : $this->generateForSpecificStoreView($storeId, $productCategories); + ? $this->generateForGlobalScope($productCategories, $product, $rootCategoryId) + : $this->generateForSpecificStoreView($storeId, $productCategories, $product, $rootCategoryId); - $this->product = null; return $urls; } /** - * Check is global scope + * Check is global scope. * + * @deprecated * @param int|null $storeId * @return bool */ protected function isGlobalScope($storeId) { - return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + return $this->getProductScopeRewriteGenerator()->isGlobalScope($storeId); } /** - * Generate list of urls for global scope + * Generate list of urls for global scope. * + * @deprecated * @param \Magento\Framework\Data\Collection $productCategories + * @param Product|null $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForGlobalScope($productCategories) + protected function generateForGlobalScope($productCategories, $product = null, $rootCategoryId = null) { - $urls = []; - $productId = $this->product->getEntityId(); - foreach ($this->product->getStoreIds() as $id) { - if (!$this->isGlobalScope($id) - && !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore($id, $productId, Product::ENTITY) - ) { - $urls = array_merge($urls, $this->generateForSpecificStoreView($id, $productCategories)); - } - } - return $urls; + return $this->getProductScopeRewriteGenerator()->generateForGlobalScope( + $productCategories, + $product, + $rootCategoryId + ); } /** - * Generate list of urls for specific store view + * Generate list of urls for specific store view. * + * @deprecated * @param int $storeId * @param \Magento\Framework\Data\Collection $productCategories + * @param Product|null $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForSpecificStoreView($storeId, $productCategories) - { - $categories = []; - foreach ($productCategories as $category) { - if ($this->isCategoryProperForGenerating($category, $storeId)) { - $categories[] = $category; - } - } - $this->productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); - /** - * @var $urls \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] - */ - $urls = array_merge( - $this->canonicalUrlRewriteGenerator->generate($storeId, $this->product), - $this->categoriesUrlRewriteGenerator->generate($storeId, $this->product, $this->productCategories), - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->productCategories), - $this->getAnchorUrlRewriteGenerator()->generate($storeId, $this->product, $this->productCategories) - ); - - /* Reduce duplicates. Last wins */ - $result = []; - foreach ($urls as $url) { - $result[$url->getTargetPath() . '-' . $url->getStoreId()] = $url; - } - $this->productCategories = null; - return $result; + protected function generateForSpecificStoreView( + $storeId, + $productCategories, + $product = null, + $rootCategoryId = null + ) { + return $this->getProductScopeRewriteGenerator() + ->generateForSpecificStoreView($storeId, $productCategories, $product, $rootCategoryId); } /** + * @deprecated * @param \Magento\Catalog\Model\Category $category * @param int $storeId * @return bool */ protected function isCategoryProperForGenerating($category, $storeId) { - if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - list(, $rootCategoryId) = $category->getParentIds(); - return $rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId(); - } - return false; + return $this->getProductScopeRewriteGenerator()->isCategoryProperForGenerating($category, $storeId); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index f2043d305c4e8..efe75e57a3f1d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -6,6 +6,7 @@ namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Category; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\UrlRewrite\Model\Storage\DbStorage; class Product extends AbstractDb { @@ -51,10 +52,13 @@ public function saveMultiple(array $insertData) foreach ($data as $insertData) { $totalCount += $connection->insertMultiple($this->getTable(self::TABLE_NAME), $insertData); } + return $totalCount; } /** + * Removes data by primary key. + * * @param array $removeData * @return int */ @@ -65,4 +69,37 @@ public function removeMultiple(array $removeData) ['url_rewrite_id in (?)' => $removeData] ); } + + /** + * Removes multiple entities from url_rewrite table using entities from catalog_url_rewrite_product_category. + * Example: $filter = ['category_id' => [1, 2, 3], 'product_id' => [1, 2, 3]] + * + * @param array $filter + * @return int + */ + public function removeMultipleByProductCategory(array $filter) + { + return $this->getConnection()->delete( + $this->getTable(self::TABLE_NAME), + ['url_rewrite_id in (?)' => $this->prepareSelect($filter)] + ); + } + + /** + * Prepare select statement for specific filter. + * + * @param array $data + * @return \Magento\Framework\DB\Select + */ + private function prepareSelect($data) + { + $select = $this->getConnection()->select(); + $select->from($this->getTable(DbStorage::TABLE_NAME), 'url_rewrite_id'); + + foreach ($data as $column => $value) { + $select->where($this->getConnection()->quoteIdentifier($column) . ' IN (?)', $value); + } + + return $select; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php b/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php new file mode 100644 index 0000000000000..48d51f6d8a9a5 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php @@ -0,0 +1,43 @@ +urlPersist = $urlPersist; + } + + /** + * Do Bunch Replace, with default bunch value = 10000. + * + * @param array $urls + * @param int $bunchSize + * @return void + */ + public function doBunchReplace(array $urls, $bunchSize = 10000) + { + foreach (array_chunk($urls, $bunchSize) as $urlsBunch) { + $this->urlPersist->replace($urlsBunch); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index a4847fa11b1c4..6a9567aac72d0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -6,11 +6,9 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\Product; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Framework\Event\Observer; -use Magento\Framework\App\ResourceConnection; use Magento\ImportExport\Model\Import as ImportExport; use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; @@ -20,6 +18,9 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; + /** * Class AfterImportDataObserver * @@ -96,6 +97,13 @@ class AfterImportDataObserver implements ObserverInterface 'visibility', ]; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\Catalog\Model\ProductFactory $catalogProductFactory * @param \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory @@ -105,6 +113,7 @@ class AfterImportDataObserver implements ObserverInterface * @param UrlPersistInterface $urlPersist * @param UrlRewriteFactory $urlRewriteFactory * @param UrlFinderInterface $urlFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory * @throws \InvalidArgumentException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -116,7 +125,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, UrlPersistInterface $urlPersist, UrlRewriteFactory $urlRewriteFactory, - UrlFinderInterface $urlFinder + UrlFinderInterface $urlFinder, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->urlPersist = $urlPersist; $this->catalogProductFactory = $catalogProductFactory; @@ -126,6 +136,10 @@ public function __construct( $this->storeManager = $storeManager; $this->urlRewriteFactory = $urlRewriteFactory; $this->urlFinder = $urlFinder; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -258,24 +272,15 @@ protected function populateGlobalProduct($product) */ protected function generateUrls() { - /** - * @var $urls \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] - */ - $urls = array_merge( - $this->canonicalUrlRewriteGenerate(), - $this->categoriesUrlRewriteGenerate(), - $this->currentUrlRewritesRegenerate() - ); - - /* Reduce duplicates. Last wins */ - $result = []; - foreach ($urls as $url) { - $result[$url->getTargetPath() . '-' . $url->getStoreId()] = $url; - } + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate()); + $mergeDataProvider->merge($this->categoriesUrlRewriteGenerate()); + $mergeDataProvider->merge($this->currentUrlRewritesRegenerate()); $this->productCategories = null; - + unset($this->products); $this->products = []; - return $result; + + return $mergeDataProvider->getData(); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php index 469b4ff78d08c..0a50ae74895ae 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php @@ -8,7 +8,9 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\Framework\Event\ObserverInterface; @@ -27,6 +29,13 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface /** @var UrlRewriteHandler */ protected $urlRewriteHandler; + /** + * Url Rewrite Replacer based on bunches. + * + * @var UrlRewriteBunchReplacer + */ + private $urlRewriteBunchReplacer; + /** * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator * @param UrlPersistInterface $urlPersist @@ -45,6 +54,21 @@ public function __construct( $this->urlRewriteHandler = $urlRewriteHandler; } + /** + * Retrieve Url Rewrite Replacer based on bunches. + * + * @deprecated + * @return UrlRewriteBunchReplacer + */ + private function getUrlRewriteBunchReplacer() + { + if (!$this->urlRewriteBunchReplacer) { + $this->urlRewriteBunchReplacer = ObjectManager::getInstance()->get(UrlRewriteBunchReplacer::class); + } + + return $this->urlRewriteBunchReplacer; + } + /** * @param \Magento\Framework\Event\Observer $observer * @return void @@ -65,7 +89,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->urlRewriteHandler->generateProductUrlRewrites($category) ); $this->urlRewriteHandler->deleteCategoryRewritesForChildren($category); - $this->urlPersist->replace($urlRewrites); + $this->getUrlRewriteBunchReplacer()->doBunchReplace($urlRewrites); } } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index b7c6109ae6ada..65400883779f3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -3,37 +3,77 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +// @codingStandardsIgnoreFile + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; use Magento\Framework\Event\ObserverInterface; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface { - /** @var CategoryUrlRewriteGenerator */ - protected $categoryUrlRewriteGenerator; + /** + * Category UrlRewrite generator. + * + * @var CategoryUrlRewriteGenerator + */ + private $categoryUrlRewriteGenerator; - /** @var UrlPersistInterface */ - protected $urlPersist; + /** + * Url Rewrite Replacer based on bunches. + * + * @var UrlRewriteBunchReplacer + */ + private $urlRewriteBunchReplacer; - /** @var UrlRewriteHandler */ - protected $urlRewriteHandler; + /** + * UrlRewrite handler. + * + * @var UrlRewriteHandler + */ + private $urlRewriteHandler; + + /** + * Pool for database maps. + * @var DatabaseMapPool + */ + private $databaseMapPool; + + /** + * Class names of maps that hold data for url rewrites entities. + * + * @var string[] + */ + private $dataUrlRewriteClassNames; /** * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist * @param UrlRewriteHandler $urlRewriteHandler + * @param UrlRewriteBunchReplacer $urlRewriteBunchReplacer + * @param DatabaseMapPool $databaseMapPool + * @param string[] $dataUrlRewriteClassNames */ public function __construct( CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, - UrlPersistInterface $urlPersist, - UrlRewriteHandler $urlRewriteHandler + UrlRewriteHandler $urlRewriteHandler, + UrlRewriteBunchReplacer $urlRewriteBunchReplacer, + DatabaseMapPool $databaseMapPool, + $dataUrlRewriteClassNames = [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ] ) { $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; - $this->urlPersist = $urlPersist; $this->urlRewriteHandler = $urlRewriteHandler; + $this->urlRewriteBunchReplacer = $urlRewriteBunchReplacer; + $this->databaseMapPool = $databaseMapPool; + $this->dataUrlRewriteClassNames = $dataUrlRewriteClassNames; } /** @@ -45,7 +85,7 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ - $category = $observer->getEvent()->getCategory(); + $category = $observer->getEvent()->getData('category'); if ($category->getParentId() == Category::TREE_ROOT_ID) { return; } @@ -53,11 +93,28 @@ public function execute(\Magento\Framework\Event\Observer $observer) || $category->dataHasChangedFor('is_anchor') || $category->getIsChangedProductList() ) { - $urlRewrites = array_merge( - $this->categoryUrlRewriteGenerator->generate($category), - $this->urlRewriteHandler->generateProductUrlRewrites($category) - ); - $this->urlPersist->replace($urlRewrites); + $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); + $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); + + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + + //frees memory for maps that are self-initialized in multiple classes that were called by the generators + $this->resetUrlRewritesDataMaps($category); + } + } + + /** + * Resets used data maps to free up memory and temporary tables. + * + * @param Category $category + * @return void + */ + private function resetUrlRewritesDataMaps($category) + { + foreach ($this->dataUrlRewriteClassNames as $className) { + $this->databaseMapPool->resetMap($className, $category->getEntityId()); } + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index f0f0ef208bf76..cc8d3cc4aecdd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -6,11 +6,14 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\CategoryBasedProductRewriteGenerator; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\Observer as EventObserver; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; class UrlRewriteHandler { @@ -32,25 +35,45 @@ class UrlRewriteHandler /** @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $productCollectionFactory; + /** + * Generates product url rewrites based on category. + * + * @var CategoryBasedProductRewriteGenerator + */ + private $categoryBasedProductRewriteGenerator; + + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider, CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; $this->productCollectionFactory = $productCollectionFactory; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -61,10 +84,10 @@ public function __construct( */ public function generateProductUrlRewrites(Category $category) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct = []; $saveRewriteHistory = $category->getData('save_rewrites_history'); $storeId = $category->getStoreId(); - $productUrls = []; if ($category->getAffectedProductIds()) { $this->isSkippedProduct = $category->getAffectedProductIds(); $collection = $this->productCollectionFactory->create() @@ -77,38 +100,54 @@ public function generateProductUrlRewrites(Category $category) foreach ($collection as $product) { $product->setStoreId($storeId); $product->setData('save_rewrites_history', $saveRewriteHistory); - $productUrls = array_merge($productUrls, $this->productUrlRewriteGenerator->generate($product)); + $mergeDataProvider->merge( + $this->productUrlRewriteGenerator->generate($product, $category->getEntityId()) + ); } } else { - $productUrls = array_merge( - $productUrls, - $this->getCategoryProductsUrlRewrites($category, $storeId, $saveRewriteHistory) + $mergeDataProvider->merge( + $this->getCategoryProductsUrlRewrites( + $category, + $storeId, + $saveRewriteHistory, + $category->getEntityId() + ) ); } foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $productUrls = array_merge( - $productUrls, - $this->getCategoryProductsUrlRewrites($childCategory, $storeId, $saveRewriteHistory) + $mergeDataProvider->merge( + $this->getCategoryProductsUrlRewrites( + $childCategory, + $storeId, + $saveRewriteHistory, + $category->getEntityId() + ) ); } - return $productUrls; + + return $mergeDataProvider->getData(); } /** * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory + * @param int|null $rootCategoryId * @return UrlRewrite[] */ - public function getCategoryProductsUrlRewrites(Category $category, $storeId, $saveRewriteHistory) - { + public function getCategoryProductsUrlRewrites( + Category $category, + $storeId, + $saveRewriteHistory, + $rootCategoryId = null + ) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ $productCollection = $category->getProductCollection() ->addAttributeToSelect('name') ->addAttributeToSelect('visibility') ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); - $productUrls = []; foreach ($productCollection as $product) { if (in_array($product->getId(), $this->isSkippedProduct)) { continue; @@ -116,9 +155,28 @@ public function getCategoryProductsUrlRewrites(Category $category, $storeId, $sa $this->isSkippedProduct[] = $product->getId(); $product->setStoreId($storeId); $product->setData('save_rewrites_history', $saveRewriteHistory); - $productUrls = array_merge($productUrls, $this->productUrlRewriteGenerator->generate($product)); + $mergeDataProvider->merge( + $this->getCategoryBasedProductRewriteGenerator()->generate($product, $category, $rootCategoryId) + ); } - return $productUrls; + + return $mergeDataProvider->getData(); + } + + /** + * Retrieve generator, which use single category for different products. + * + * @deprecated + * @return CategoryBasedProductRewriteGenerator|mixed + */ + private function getCategoryBasedProductRewriteGenerator() + { + if (!$this->categoryBasedProductRewriteGenerator) { + $this->categoryBasedProductRewriteGenerator = ObjectManager::getInstance() + ->get(CategoryBasedProductRewriteGenerator::class); + } + + return $this->categoryBasedProductRewriteGenerator; } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php index da3b0435ebd37..f41d35f7688f3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php @@ -10,45 +10,59 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator */ - protected $childrenUrlRewriteGenerator; + private $childrenUrlRewriteGenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenCategoriesProvider; + private $childrenCategoriesProvider; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlRewriteGeneratorFactory; + private $categoryUrlRewriteGeneratorFactory; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlRewriteGenerator; + private $categoryUrlRewriteGenerator; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; protected function setUp() { $this->childrenCategoriesProvider = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider::class )->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $this->categoryUrlRewriteGeneratorFactory = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory' + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory::class )->disableOriginalConstructor()->setMethods(['create'])->getMock(); $this->categoryUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + $this->childrenUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator::class, [ 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, - 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory + 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } public function testNoChildrenCategories() { - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, false) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) ->will($this->returnValue([])); $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); @@ -59,23 +73,33 @@ public function testGenerate() $storeId = 'store_id'; $saveRewritesHistory = 'flag'; - $childCategory = $this->getMockBuilder('Magento\Catalog\Model\Category') + $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $childCategory->expects($this->once())->method('setStoreId')->with($storeId); $childCategory->expects($this->once())->method('setData') ->with('save_rewrites_history', $saveRewritesHistory); - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, false) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) ->will($this->returnValue([$childCategory])); $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') ->will($this->returnValue($saveRewritesHistory)); $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') ->will($this->returnValue($this->categoryUrlRewriteGenerator)); - $this->categoryUrlRewriteGenerator->expects($this->once())->method('generate')->with($childCategory) - ->will($this->returnValue([['url-1', 'url-2']])); + $url1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url1->setRequestPath('category-1') + ->setStoreId(1); + $url2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url2->setRequestPath('category-2') + ->setStoreId(2); + $url3 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url3->setRequestPath('category-1') + ->setStoreId(1); + $this->categoryUrlRewriteGenerator->expects($this->once())->method('generate') + ->with($childCategory, false, 1) + ->will($this->returnValue([$url1, $url2, $url3])); $this->assertEquals( - [['url-1', 'url-2']], - $this->childrenUrlRewriteGenerator->generate($storeId, $this->category) + ['category-1_1' => $url1, 'category-2_2' => $url2], + $this->childrenUrlRewriteGenerator->generate($storeId, $this->category, 1) ); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php index d97a14ae56b68..10d78f5c92df4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php @@ -3,7 +3,6 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; @@ -14,70 +13,79 @@ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator */ - protected $currentUrlRewritesRegenerator; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $filter; - - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + private $currentUrlRewritesRegenerator; /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + private $categoryUrlPathGenerator; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $urlRewriteFinder; protected function setUp() { - $this->urlRewriteFactory = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory') + $this->urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class) ->setMethods(['create']) ->disableOriginalConstructor()->getMock(); - $this->urlRewrite = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') - ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); - $this->filter = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\Filter') - ->disableOriginalConstructor()->getMock(); - $this->filter->expects($this->any())->method('setStoreId')->will($this->returnSelf()); - $this->filter->expects($this->any())->method('setEntityId')->will($this->returnSelf()); - $this->urlFinder = $this->getMockBuilder('Magento\UrlRewrite\Model\UrlFinderInterface') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $this->categoryUrlPathGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator' + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class )->disableOriginalConstructor()->getMock(); + $this->urlRewriteFinder = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class) + ->disableOriginalConstructor()->getMock(); + $this->urlRewriteFactory->expects($this->once())->method('create') + ->willReturn($this->urlRewrite); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator', + \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator::class, [ - 'urlFinder' => $this->urlFinder, 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, - 'urlRewriteFactory' => $this->urlRewriteFactory + 'urlRewriteFactory' => $this->urlRewriteFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'urlRewriteFinder' => $this->urlRewriteFinder ] ); } public function testIsAutogeneratedWithoutSaveRewriteHistory() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([[UrlRewrite::IS_AUTOGENERATED => 1]]))); $this->category->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(false)); $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } public function testSkipGenerationForAutogenerated() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -94,7 +102,7 @@ public function testSkipGenerationForAutogenerated() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } @@ -104,7 +112,7 @@ public function testIsAutogenerated() $targetPath = 'some-path.html'; $storeId = 2; $categoryId = 12; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -120,23 +128,23 @@ public function testIsAutogenerated() ) ) ); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->category->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, OptionProvider::PERMANENT); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, OptionProvider::PERMANENT, 0); $this->assertEquals( ['autogenerated.html_2' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } public function testSkipGenerationForCustom() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -155,7 +163,7 @@ public function testSkipGenerationForCustom() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } @@ -166,7 +174,7 @@ public function testGenerationForCustomWithoutTargetPathGeneration() $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'custom-target-path.html'; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -184,16 +192,14 @@ public function testGenerationForCustomWithoutTargetPathGeneration() ) ); $this->categoryUrlPathGenerator->expects($this->never())->method('getUrlPathWithSuffix'); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->urlRewrite->expects($this->once())->method('setDescription')->with($description) ->will($this->returnSelf()); - $this->urlRewriteFactory->expects($this->once())->method('create') - ->willReturn($this->urlRewrite); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 0); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 0, 0); $this->assertEquals( ['generate-for-custom-without-redirect-type.html_12' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } @@ -204,7 +210,7 @@ public function testGenerationForCustomWithTargetPathGeneration() $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'generated-target-path.html'; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -223,16 +229,14 @@ public function testGenerationForCustomWithTargetPathGeneration() ); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->urlRewrite->expects($this->once())->method('setDescription')->with($description) ->will($this->returnSelf()); - $this->urlRewriteFactory->expects($this->once())->method('create') - ->willReturn($this->urlRewrite); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 'code'); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 'code', 0); $this->assertEquals( ['generate-for-custom-without-redirect-type.html_12' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } @@ -245,7 +249,7 @@ protected function getCurrentRewritesMocks($currentRewrites) $rewrites = []; foreach ($currentRewrites as $urlRewrite) { /** @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) @@ -263,9 +267,16 @@ protected function getCurrentRewritesMocks($currentRewrites) * @param mixed $requestPath * @param mixed $targetPath * @param mixed $redirectType + * @param int $isAutoGenerated */ - protected function prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, $redirectType) - { + protected function prepareUrlRewriteMock( + $storeId, + $categoryId, + $requestPath, + $targetPath, + $redirectType, + $isAutoGenerated + ) { $this->urlRewrite->expects($this->any())->method('setStoreId')->with($storeId) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setEntityId')->with($categoryId) @@ -276,11 +287,14 @@ protected function prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $t ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setTargetPath')->with($targetPath) ->will($this->returnSelf()); - $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with(0) + $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with($isAutoGenerated) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setRedirectType')->with($redirectType) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setMetadata')->with([])->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('getTargetPath')->willReturn($targetPath); + $this->urlRewrite->expects($this->any())->method('getRequestPath')->willReturn($requestPath); + $this->urlRewrite->expects($this->any())->method('getStoreId')->willReturn($storeId); $this->urlRewriteFactory->expects($this->any())->method('create')->will($this->returnValue($this->urlRewrite)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php new file mode 100644 index 0000000000000..82f7863592410 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php @@ -0,0 +1,122 @@ +storage = $this->getMockBuilder(StorageInterface::class) + ->getMockForAbstractClass(); + $this->urlFinder = $this->getMockBuilder(UrlFinderInterface::class) + ->getMockForAbstractClass(); + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productResourceModel = $this->getMockBuilder(ProductResourceModel::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlRewrite = $this->getMockBuilder(UrlRewrite::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata', 'getEntityType', 'getIsAutogenerated', 'getUrlRewriteId', 'getEntityId']) + ->getMock(); + + $this->plugin = (new ObjectManager($this))->getObject( + CategoryStoragePlugin::class, + [ + 'urlFinder' => $this->urlFinder, + 'productResource' => $this->productResourceModel + ] + ); + } + + /** + * Covers afterReplace() method. + * + * @return void + */ + public function testAroundReplace() + { + $this->urlRewrite->expects(static::any())->method('getMetadata')->willReturn(['category_id' => '5']); + $this->urlRewrite->expects(static::once())->method('getEntityTYpe')->willReturn('product'); + $this->urlRewrite->expects(static::once())->method('getIsAutogenerated')->willReturn(1); + $this->urlRewrite->expects(static::once())->method('getUrlRewriteId')->willReturn('4'); + $this->urlRewrite->expects(static::once())->method('getEntityId')->willReturn('2'); + $this->urlRewrite->setData('request_path', 'test'); + $this->urlRewrite->setData('store_id', '1'); + $productUrls = ['targetPath' => $this->urlRewrite]; + + $this->urlFinder->expects(static::once())->method('findAllByData')->willReturn([$this->urlRewrite]); + + $this->productResourceModel->expects(static::once())->method('saveMultiple')->willReturnSelf(); + + $className = 'Magento\UrlRewrite\Model\StorageInterface'; + /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $subject */ + $subject = $this->getMock($className, [], [], '', false); + $closureMock = function () use ($subject) { + return $subject; + }; + + $this->plugin->aroundReplace($this->storage, $closureMock, $productUrls); + } + + /** + * Covers beforeDeleteByData() method. + * + * @return void + */ + public function testBeforeDeleteByData() + { + $data = [1, 2, 3]; + $this->productResourceModel->expects(static::once()) + ->method('removeMultipleByProductCategory') + ->with($data)->willReturnSelf(); + $this->plugin->beforeDeleteByData($this->storage, $data); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php new file mode 100644 index 0000000000000..1b4029b112268 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php @@ -0,0 +1,114 @@ +productScopeRewriteGeneratorMock = $this->getMockBuilder(ProductScopeRewriteGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->generator = new CategoryBasedProductRewriteGenerator( + $this->productScopeRewriteGeneratorMock + ); + } + + /** + * Covers generate() with global scope. + * + * @return void + */ + public function testGenerationWithGlobalScope() + { + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $categoryId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('isGlobalScope') + ->with($storeId) + ->willReturn(true); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('generateForGlobalScope') + ->with([$categoryMock], $productMock, $categoryId) + ->willReturn($urls); + + $this->assertEquals($urls, $this->generator->generate($productMock, $categoryMock, $categoryId)); + } + + /** + * Covers generate() with specific store. + * + * @return void + */ + public function testGenerationWithSpecificStore() + { + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $categoryId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('isGlobalScope') + ->with($storeId) + ->willReturn(false); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('generateForSpecificStoreView') + ->with($storeId, [$categoryMock], $productMock, $categoryId) + ->willReturn($urls); + + $this->assertEquals($urls, $this->generator->generate($productMock, $categoryMock, $categoryId)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php index d273947629f8b..c3a2ebb47b156 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php @@ -14,25 +14,28 @@ class CategoryUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $canonicalUrlRewriteGenerator; + private $canonicalUrlRewriteGenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $currentUrlRewritesRegenerator; + private $currentUrlRewritesRegenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenUrlRewriteGenerator; + private $childrenUrlRewriteGenerator; /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator */ - protected $categoryUrlRewriteGenerator; + private $categoryUrlRewriteGenerator; /** @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepository; + private $categoryRepository; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; /** * Test method @@ -40,27 +43,37 @@ class CategoryUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->currentUrlRewritesRegenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator' + \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator::class )->disableOriginalConstructor()->getMock(); $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); $this->childrenUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); - $this->storeViewService = $this->getMockBuilder('Magento\CatalogUrlRewrite\Service\V1\StoreViewService') + $this->storeViewService = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $this->categoryRepository = $this->getMock('Magento\Catalog\Api\CategoryRepositoryInterface'); + $this->category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); + $this->categoryRepository = $this->getMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->categoryUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class, [ 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, 'childrenUrlRewriteGenerator' => $this->childrenUrlRewriteGenerator, 'currentUrlRewritesRegenerator' => $this->currentUrlRewritesRegenerator, 'storeViewService' => $this->storeViewService, 'categoryRepository' => $this->categoryRepository, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } @@ -70,27 +83,33 @@ protected function setUp() */ public function testGenerationForGlobalScope() { + $categoryId = 1; $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); $this->category->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') ->will($this->returnValue(false)); $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') + $canonical->setRequestPath('category-1') ->setStoreId(1); $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $children = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $children->setTargetPath('category-2') + ->will($this->returnValue(['category-1' => $canonical])); + $children1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $children1->setRequestPath('category-2') + ->setStoreId(2); + $children2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $children2->setRequestPath('category-22') ->setStoreId(2); $this->childrenUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$children])); + ->with(1, $this->category, $categoryId) + ->will($this->returnValue(['category-2' => $children1, 'category-1' => $children2])); $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $current->setTargetPath('category-3') + $current->setRequestPath('category-3') ->setStoreId(3); $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$current])); + ->with(1, $this->category, $categoryId) + ->will($this->returnValue(['category-3' => $current])); $categoryForSpecificStore = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getUrlKey', 'getUrlPath'], [], '', @@ -99,8 +118,13 @@ public function testGenerationForGlobalScope() $this->categoryRepository->expects($this->once())->method('get')->willReturn($categoryForSpecificStore); $this->assertEquals( - [$canonical, $children, $current], - $this->categoryUrlRewriteGenerator->generate($this->category) + [ + 'category-1_1' => $canonical, + 'category-2_2' => $children1, + 'category-22_2' => $children2, + 'category-3_3' => $current + ], + $this->categoryUrlRewriteGenerator->generate($this->category, false, $categoryId) ); } @@ -112,7 +136,7 @@ public function testGenerationForSpecificStore() $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $this->category->expects($this->never())->method('getStoreIds'); $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') + $canonical->setRequestPath('category-1') ->setStoreId(1); $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') ->will($this->returnValue([$canonical])); @@ -121,7 +145,10 @@ public function testGenerationForSpecificStore() $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') ->will($this->returnValue([])); - $this->assertEquals([$canonical], $this->categoryUrlRewriteGenerator->generate($this->category)); + $this->assertEquals( + ['category-1_1' => $canonical], + $this->categoryUrlRewriteGenerator->generate($this->category, 1) + ); } /** @@ -135,4 +162,18 @@ public function testSkipGenerationForGlobalScope() $this->assertEquals([], $this->categoryUrlRewriteGenerator->generate($this->category)); } + + /** + * Test method + */ + public function testSkipGenerationForGlobalScopeWithCategory() + { + $this->category->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue(1)); + $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(false)); + $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(true)); + + $this->assertEquals([], $this->categoryUrlRewriteGenerator->generate($this->category, false, 1)); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php new file mode 100644 index 0000000000000..aaec181834e07 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php @@ -0,0 +1,104 @@ +categoryRepository = $this->getMock(CategoryRepository::class, [], [], '', false); + $this->categoryResourceFactory = $this->getMock(CategoryFactory::class, ['create'], [], '', false); + $this->categoryResource = $this->getMock( + Category::class, + ['getConnection', 'getEntityTable'], + [], + '', + false + ); + + $this->categoryResourceFactory->expects($this->any()) + ->method('create') + ->willReturn($this->categoryResource); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryHashMap::class, + [ + 'categoryRepository' => $this->categoryRepository, + 'categoryResourceFactory' => $this->categoryResourceFactory + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality. + */ + public function testGetAllData() + { + $categoryIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $categoryIdsOther = ['2' => [2, 3, 4]]; + + $categoryMock = $this->getMock(CategoryInterface::class, [], [], '', false); + $connectionAdapterMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->categoryRepository->expects($this->any()) + ->method('get') + ->willReturn($categoryMock); + $categoryMock->expects($this->any()) + ->method('getResource') + ->willReturn($this->categoryResource); + $this->categoryResource->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionAdapterMock); + $this->categoryResource->expects($this->any()) + ->method('getEntityTable') + ->willReturn('category_entity'); + $connectionAdapterMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $connectionAdapterMock->expects($this->any()) + ->method('fetchCol') + ->willReturnOnConsecutiveCalls($categoryIds, $categoryIdsOther, $categoryIds); + + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIdsOther, $this->model->getAllData(2)); + $this->assertEquals($categoryIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php new file mode 100644 index 0000000000000..549a2db774208 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php @@ -0,0 +1,125 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->dataCategoryUsedInProductsMapMock = $this->getMock( + DataCategoryUsedInProductsHashMap::class, + [], + [], + '', + false + ); + $this->temporaryTableServiceMock = $this->getMock(TemporaryTableService::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturnOnConsecutiveCalls($this->dataCategoryUsedInProductsMapMock, $this->dataCategoryMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryUrlRewriteDatabaseMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock, + 'temporaryTableService' => $this->temporaryTableServiceMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productStoreIds = [ + '1' => ['store_id' => 1, 'category_id' => 1], + '2' => ['store_id' => 2, 'category_id' => 1], + '3' => ['store_id' => 3, 'category_id' => 1], + '4' => ['store_id' => 1, 'category_id' => 2], + '5' => ['store_id' => 2, 'category_id' => 2], + ]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchAll') + ->willReturn($productStoreIds[3]); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->dataCategoryMapMock->expects($this->once()) + ->method('getAllData') + ->willReturn([]); + $this->dataCategoryUsedInProductsMapMock->expects($this->once()) + ->method('getAllData') + ->willReturn([]); + $this->temporaryTableServiceMock->expects($this->any()) + ->method('createFromSelect') + ->withConsecutive( + $selectMock, + $connectionMock, + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ) + ->willReturn('tempTableName'); + + $this->assertEquals($productStoreIds[3], $this->model->getData(1, '3_1')); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php new file mode 100644 index 0000000000000..e04aef2e97545 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php @@ -0,0 +1,108 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $this->dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturnOnConsecutiveCalls( + $this->dataProductMapMock, + $this->dataCategoryMapMock, + $this->dataProductMapMock, + $this->dataCategoryMapMock, + $this->dataProductMapMock, + $this->dataCategoryMapMock + ); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryUsedInProductsHashMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $categoryIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $categoryIdsOther = ['2' => [2, 3, 4]]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchCol') + ->willReturnOnConsecutiveCalls($categoryIds, $categoryIdsOther, $categoryIds); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->hashMapPoolMock->expects($this->at(4)) + ->method('resetMap') + ->with(DataProductHashMap::class, 1); + $this->hashMapPoolMock->expects($this->at(5)) + ->method('resetMap') + ->with(DataCategoryHashMap::class, 1); + + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIdsOther, $this->model->getAllData(2)); + $this->assertEquals($categoryIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php new file mode 100644 index 0000000000000..7b4e06902a1df --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php @@ -0,0 +1,121 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $this->collectionFactoryMock = $this->getMock(CollectionFactory::class, ['create'], [], '', false); + $this->productCollectionMock = $this->getMock( + ProductCollection::class, + ['getSelect', 'getConnection', 'getAllIds'], + [], + '', + false + ); + + $this->collectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->productCollectionMock); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturn($this->dataCategoryMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataProductHashMap::class, + [ + 'collectionFactory' => $this->collectionFactoryMock, + 'hashMapPool' => $this->hashMapPoolMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $productIdsOther = ['2' => [2, 3, 4]]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->productCollectionMock->expects($this->exactly(3)) + ->method('getAllIds') + ->willReturnOnConsecutiveCalls($productIds, $productIdsOther, $productIds); + $this->productCollectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('getTableName') + ->willReturn($this->returnValue($this->returnArgument(0))); + $this->productCollectionMock->expects($this->any()) + ->method('getSelect') + ->willReturn($selectMock); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->dataCategoryMapMock->expects($this->any()) + ->method('getAllData') + ->willReturn([]); + $this->hashMapPoolMock->expects($this->any()) + ->method('resetMap') + ->with(DataCategoryHashMap::class, 1); + $this->assertEquals($productIds, $this->model->getAllData(1)); + $this->assertEquals($productIds[2], $this->model->getData(1, 2)); + $this->assertEquals($productIdsOther, $this->model->getAllData(2)); + $this->assertEquals($productIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($productIds[2], $this->model->getData(1, 2)); + $this->assertEquals($productIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php new file mode 100644 index 0000000000000..eaabc9ca99318 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php @@ -0,0 +1,112 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->temporaryTableServiceMock = $this->getMock(TemporaryTableService::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturn($this->dataProductMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataProductUrlRewriteDatabaseMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock, + 'temporaryTableService' => $this->temporaryTableServiceMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productStoreIds = [ + '1' => ['store_id' => 1, 'product_id' => 1], + '2' => ['store_id' => 2, 'product_id' => 1], + '3' => ['store_id' => 3, 'product_id' => 1], + '4' => ['store_id' => 1, 'product_id' => 2], + '5' => ['store_id' => 2, 'product_id' => 2], + ]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchAll') + ->willReturn($productStoreIds[3]); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + + $this->dataProductMapMock->expects($this->any()) + ->method('getAllData') + ->willReturn([]); + + $this->temporaryTableServiceMock->expects($this->any()) + ->method('createFromSelect') + ->withConsecutive( + $selectMock, + $connectionMock, + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ) + ->willReturn('tempTableName'); + + $this->assertEquals($productStoreIds[3], $this->model->getData(1, '3_1')); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php new file mode 100644 index 0000000000000..86ad2f53f647c --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php @@ -0,0 +1,78 @@ +objectManagerMock = $this->getMock(ObjectManagerInterface::class); + + $this->model = (new ObjectManager($this))->getObject( + DatabaseMapPool::class, + [ + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Tests getDataMap(). + */ + public function testGetDataMap() + { + $dataCategoryMapMock = $this->getMock(DataCategoryUrlRewriteDatabaseMap::class, [], [], '', false); + $dataProductMapMock = $this->getMock(DataProductUrlRewriteDatabaseMap::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturnMap( + [ + [ + DataCategoryUrlRewriteDatabaseMap::class, + ['category' => 1], + $dataCategoryMapMock + ], + [ + DataProductUrlRewriteDatabaseMap::class, + ['category' => 1], + $dataProductMapMock + ] + ] + ); + $this->assertSame($dataCategoryMapMock, $this->model->getDataMap(DataCategoryUrlRewriteDatabaseMap::class, 1)); + $this->assertSame($dataProductMapMock, $this->model->getDataMap(DataProductUrlRewriteDatabaseMap::class, 1)); + } + + /** + * Tests getDataMap() with exception. + */ + public function testGetDataMapException() + { + $nonInterface = $this->getMock(DatabaseMapPool::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturn($nonInterface); + $this->setExpectedException(\InvalidArgumentException::class); + $this->model->getDataMap(DatabaseMapPool::class, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php new file mode 100644 index 0000000000000..f6540d74a94b4 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php @@ -0,0 +1,89 @@ +objectManagerMock = $this->getMock(ObjectManagerInterface::class); + + $this->model = (new ObjectManager($this))->getObject( + HashMapPool::class, + [ + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Tests getDataMap(). + */ + public function testGetDataMap() + { + $dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $dataProductMapMockOtherCategory = $this->getMock(DataCategoryUsedInProductsHashMap::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturnMap( + [ + [ + DataCategoryHashMap::class, + ['category' => 1], + $dataCategoryMapMock + ], + [ + DataProductHashMap::class, + ['category' => 1], + $dataProductMapMock + ], + [ + DataCategoryUsedInProductsHashMap::class, + ['category' => 2], + $dataProductMapMockOtherCategory + ] + ] + ); + $this->assertSame($dataCategoryMapMock, $this->model->getDataMap(DataCategoryHashMap::class, 1)); + $this->assertSame($dataProductMapMock, $this->model->getDataMap(DataProductHashMap::class, 1)); + $this->assertSame( + $dataProductMapMockOtherCategory, + $this->model->getDataMap(DataCategoryUsedInProductsHashMap::class, 2) + ); + } + + /** + * Tests getDataMap() with exception. + */ + public function testGetDataMapException() + { + $nonInterface = $this->getMock(HashMapPool::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturn($nonInterface); + $this->setExpectedException(\InvalidArgumentException::class); + $this->model->getDataMap(HashMapPool::class, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php new file mode 100644 index 0000000000000..47647048dc828 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php @@ -0,0 +1,167 @@ +databaseMapPoolMock = $this->getMock(DatabaseMapPool::class, [], [], '', false); + $this->urlFinderMock = $this->getMock(UrlFinderInterface::class); + $this->urlRewriteFactoryMock = $this->getMock(UrlRewriteFactory::class, ['create'], [], '', false); + $this->urlRewritePrototypeMock = new UrlRewrite(); + + $this->urlRewriteFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->urlRewritePrototypeMock); + + $urlRewriteClassesNamesArray = [ + UrlRewriteFinder::ENTITY_TYPE_PRODUCT => DataProductUrlRewriteDatabaseMap::class, + UrlRewriteFinder::ENTITY_TYPE_CATEGORY => DataCategoryUrlRewriteDatabaseMap::class + ]; + + $this->model = (new ObjectManager($this))->getObject( + UrlRewriteFinder::class, + [ + 'databaseMapPool' => $this->databaseMapPoolMock, + 'urlFinder' => $this->urlFinderMock, + 'urlRewriteFactory' => $this->urlRewriteFactoryMock, + 'urlRewriteClassNames' => $urlRewriteClassesNamesArray + ] + ); + } + + /** + * Covers findAllByData() using urlFinder. + * + * @return void + */ + public function testGetByIdentifiersFallback() + { + $expected = [1, 2, 3]; + $this->databaseMapPoolMock->expects($this->never()) + ->method('getDataMap'); + + $this->urlFinderMock->expects($this->exactly(7)) + ->method('findAllByData') + ->willReturn($expected); + + $this->assertEquals($expected, $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_CATEGORY)); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT)); + $this->assertEquals($expected, $this->model->findAllByData('a', 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData('a', 'a', UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData(1, 'a', UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, 'cms', 1)); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, 'cms')); + } + + /** + * Covers findAllByData() Product URL rewrites. + * + * @return void + */ + public function testGetByIdentifiersProduct() + { + $data =[ + [ + 'url_rewrite_id' => '1', + 'entity_type' => 'product', + 'entity_id' => '3', + 'request_path' => 'request_path', + 'target_path' => 'target_path', + 'redirect_type' => 'redirect_type', + 'store_id' => '4', + 'description' => 'description', + 'is_autogenerated' => '1', + 'metadata' => '{}' + ] + ]; + + $dataProductMapMock = $this->getMock(DataProductUrlRewriteDatabaseMap::class, [], [], '', false); + $this->databaseMapPoolMock->expects($this->once()) + ->method('getDataMap') + ->with(DataProductUrlRewriteDatabaseMap::class, 1) + ->willReturn($dataProductMapMock); + + $this->urlFinderMock->expects($this->never()) + ->method('findAllByData') + ->willReturn([]); + + $dataProductMapMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $urlRewriteResultArray = $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT, 1); + $this->assertEquals($data[0], $urlRewriteResultArray[0]->toArray()); + } + + /** + * Covers findAllByData() Category URL rewrites. + * + * @return void + */ + public function testGetByIdentifiersCategory() + { + $data =[ + [ + 'url_rewrite_id' => '1', + 'entity_type' => 'category', + 'entity_id' => '3', + 'request_path' => 'request_path', + 'target_path' => 'target_path', + 'redirect_type' => 'redirect_type', + 'store_id' => '4', + 'description' => 'description', + 'is_autogenerated' => '1', + 'metadata' => '{}' + ] + ]; + + $dataCategoryMapMock = $this->getMock(DataCategoryUrlRewriteDatabaseMap::class, [], [], '', false); + $this->databaseMapPoolMock->expects($this->once()) + ->method('getDataMap') + ->with(DataCategoryUrlRewriteDatabaseMap::class, 1) + ->willReturn($dataCategoryMapMock); + + $this->urlFinderMock->expects($this->never()) + ->method('findAllByData') + ->willReturn([]); + + $dataCategoryMapMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $urlRewriteResultArray = $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_CATEGORY, 1); + $this->assertEquals($data[0], $urlRewriteResultArray[0]->toArray()); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php index 9daaa8dc44851..b5515c2c25459 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php @@ -10,70 +10,81 @@ use Magento\UrlRewrite\Model\OptionProvider; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ - protected $currentUrlRewritesRegenerator; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $filter; - - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + private $currentUrlRewritesRegenerator; /** @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlPathGenerator; + private $productUrlPathGenerator; /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; + private $product; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistry; + private $objectRegistry; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $urlRewriteFinder; protected function setUp() { - $this->urlRewriteFactory = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory') + $this->urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class) ->setMethods(['create']) ->disableOriginalConstructor()->getMock(); - $this->urlRewrite = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') - ->disableOriginalConstructor()->getMock(); - $this->product = $this->getMockBuilder('Magento\Catalog\Model\Product') + $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor()->getMock(); - $this->objectRegistry = $this->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ObjectRegistry') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); - $this->filter = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\Filter') + $this->objectRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) ->disableOriginalConstructor()->getMock(); - $this->filter->expects($this->any())->method('setStoreId')->will($this->returnSelf()); - $this->filter->expects($this->any())->method('setEntityId')->will($this->returnSelf()); - $this->urlFinder = $this->getMockBuilder('Magento\UrlRewrite\Model\UrlFinderInterface') + $this->urlRewriteFinder = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class) ->disableOriginalConstructor()->getMock(); + $this->urlRewriteFactory->expects($this->once())->method('create') + ->willReturn($this->urlRewrite); $this->productUrlPathGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator' + \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class )->disableOriginalConstructor()->getMock(); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator', + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class, [ - 'urlFinder' => $this->urlFinder, 'productUrlPathGenerator' => $this->productUrlPathGenerator, - 'urlRewriteFactory' => $this->urlRewriteFactory + 'urlRewriteFactory' => $this->urlRewriteFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'urlRewriteFinder' => $this->urlRewriteFinder ] ); } public function testIsAutogeneratedWithoutSaveRewriteHistory() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([[UrlRewrite::IS_AUTOGENERATED => 1]]))); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(false)); @@ -86,7 +97,7 @@ public function testIsAutogeneratedWithoutSaveRewriteHistory() public function testSkipGenerationForAutogenerated() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [UrlRewrite::IS_AUTOGENERATED => 1, UrlRewrite::REQUEST_PATH => 'same-path'], ]))); @@ -97,7 +108,7 @@ public function testSkipGenerationForAutogenerated() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->product, $this->objectRegistry, 1) ); } @@ -105,21 +116,22 @@ public function testIsAutogeneratedWithoutCategory() { $requestPath = 'autogenerated.html'; $targetPath = 'some-path.html'; + $autoGenerated = 1; $storeId = 2; $productId = 12; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'custom-target-path', UrlRewrite::STORE_ID => $storeId, - UrlRewrite::IS_AUTOGENERATED => 1, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::METADATA => [], UrlRewrite::DESCRIPTION => $description, ], ]))); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->productUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') @@ -130,6 +142,7 @@ public function testIsAutogeneratedWithoutCategory() $productId, $requestPath, $targetPath, + 0, OptionProvider::PERMANENT, [], $description @@ -137,7 +150,7 @@ public function testIsAutogeneratedWithoutCategory() $this->assertEquals( [$this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry, 1) ); } @@ -146,21 +159,22 @@ public function testIsAutogeneratedWithCategory() $productId = 12; $requestPath = 'autogenerated.html'; $targetPath = 'simple-product.html'; + $autoGenerated = 1; $storeId = 2; $metadata = ['category_id' => 2, 'some_another_data' => 1]; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'some-path.html', UrlRewrite::STORE_ID => $storeId, - UrlRewrite::IS_AUTOGENERATED => 1, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::METADATA => $metadata, UrlRewrite::DESCRIPTION => $description, ], ]))); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->productUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') @@ -171,6 +185,7 @@ public function testIsAutogeneratedWithCategory() $productId, $requestPath, $targetPath, + 0, OptionProvider::PERMANENT, $metadata, $description @@ -178,13 +193,13 @@ public function testIsAutogeneratedWithCategory() $this->assertEquals( [$this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry, 2) ); } public function testSkipGenerationForCustom() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::IS_AUTOGENERATED => 0, @@ -207,21 +222,31 @@ public function testGenerationForCustomWithoutTargetPathGeneration() $productId = 123; $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'custom-target-path.html'; + $autoGenerated = 0; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => $targetPath, UrlRewrite::REDIRECT_TYPE => 0, - UrlRewrite::IS_AUTOGENERATED => 0, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::DESCRIPTION => $description, UrlRewrite::METADATA => [], ], ]))); $this->productUrlPathGenerator->expects($this->never())->method('getUrlPathWithSuffix'); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); - $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 0, [], $description); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); + $this->prepareUrlRewriteMock( + $storeId, + $productId, + $requestPath, + $targetPath, + $autoGenerated, + 0, + [], + $description + ); $this->assertEquals( [$this->urlRewrite], @@ -235,22 +260,23 @@ public function testGenerationForCustomWithTargetPathGeneration() $productId = 123; $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'generated-target-path.html'; + $autoGenerated = 0; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'custom-target-path.html', UrlRewrite::REDIRECT_TYPE => 'code', - UrlRewrite::IS_AUTOGENERATED => 0, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::DESCRIPTION => $description, UrlRewrite::METADATA => [], ], ]))); $this->productUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); - $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 'code', [], $description); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); + $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 0, 'code', [], $description); $this->assertEquals( [$this->urlRewrite], @@ -267,7 +293,7 @@ protected function getCurrentRewritesMocks($currentRewrites) $rewrites = []; foreach ($currentRewrites as $urlRewrite) { /** @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) @@ -284,6 +310,7 @@ protected function getCurrentRewritesMocks($currentRewrites) * @param mixed $productId * @param mixed $requestPath * @param mixed $targetPath + * @param mixed $autoGenerated * @param mixed $redirectType * @param mixed $metadata * @param mixed $description @@ -293,6 +320,7 @@ protected function prepareUrlRewriteMock( $productId, $requestPath, $targetPath, + $autoGenerated, $redirectType, $metadata, $description @@ -307,7 +335,7 @@ protected function prepareUrlRewriteMock( ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setTargetPath')->with($targetPath) ->will($this->returnSelf()); - $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with(0) + $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with($autoGenerated) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setRedirectType')->with($redirectType) ->will($this->returnSelf()); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php new file mode 100644 index 0000000000000..da117a0d4a37a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -0,0 +1,194 @@ +currentUrlRewritesRegenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class + )->disableOriginalConstructor()->getMock(); + $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->categoriesUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->anchorUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->objectRegistryFactory = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class + )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + $this->storeViewService = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class) + ->disableOriginalConstructor()->getMock(); + $this->storeManager = $this->getMock(StoreManagerInterface::class); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + + $this->productScopeGenerator = (new ObjectManager($this))->getObject( + \Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator::class, + [ + 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, + 'categoriesUrlRewriteGenerator' => $this->categoriesUrlRewriteGenerator, + 'currentUrlRewritesRegenerator' => $this->currentUrlRewritesRegenerator, + 'anchorUrlRewriteGenerator' => $this->anchorUrlRewriteGenerator, + 'objectRegistryFactory' => $this->objectRegistryFactory, + 'storeViewService' => $this->storeViewService, + 'storeManager' => $this->storeManager, + 'mergeDataProviderFactory' => $mergeDataProviderFactory + ] + ); + } + + public function testGenerationForGlobalScope() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); + $product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); + $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(false)); + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $categoryMock->expects($this->once()) + ->method('getParentId') + ->willReturn(1); + $this->initObjectRegistryFactory([]); + $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $canonical->setRequestPath('category-1') + ->setStoreId(1); + $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$canonical])); + $categories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categories->setRequestPath('category-2') + ->setStoreId(2); + $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$categories])); + $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $current->setRequestPath('category-3') + ->setStoreId(3); + $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$current])); + $anchorCategories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $anchorCategories->setRequestPath('category-4') + ->setStoreId(4); + $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$anchorCategories])); + + $this->assertEquals( + [ + 'category-1_1' => $canonical, + 'category-2_2' => $categories, + 'category-3_3' => $current, + 'category-4_4' => $anchorCategories + ], + $this->productScopeGenerator->generateForGlobalScope([$categoryMock], $product, 1) + ); + } + + public function testGenerationForSpecificStore() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); + $product->expects($this->never())->method('getStoreIds'); + $storeRootCategoryId = 'root-for-store-id'; + $category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); + $category->expects($this->any())->method('getParentIds') + ->will($this->returnValue(['root-id', $storeRootCategoryId])); + $category->expects($this->any())->method('getParentId')->will($this->returnValue('parent_id')); + $category->expects($this->any())->method('getId')->will($this->returnValue('category_id')); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); + $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue($storeRootCategoryId)); + $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); + $this->initObjectRegistryFactory([$category]); + $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $canonical->setRequestPath('category-1') + ->setStoreId(1); + $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$canonical])); + $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + + $this->assertEquals( + ['category-1_1' => $canonical], + $this->productScopeGenerator->generateForSpecificStoreView(1, [$category], $product, 1) + ); + } + + /** + * Test method + */ + public function testSkipGenerationForGlobalScope() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); + $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(true)); + + $this->assertEquals([], $this->productScopeGenerator->generateForGlobalScope([], $product, 1)); + } + + /** + * @param array $entities + */ + protected function initObjectRegistryFactory($entities) + { + $objectRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) + ->disableOriginalConstructor()->getMock(); + $this->objectRegistryFactory->expects($this->any())->method('create') + ->with(['entities' => $entities]) + ->will($this->returnValue($objectRegistry)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php index 3798b5d602153..985ad15e1b6b1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php @@ -4,13 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogUrlRewrite\Test\Unit\Model; -use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests ProductUrlRewriteGenerator class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ProductUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ @@ -43,39 +47,66 @@ class ProductUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection|\PHPUnit_Framework_MockObject_MockObject */ protected $categoriesCollection; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $productScopeRewriteGenerator; + /** * Test method */ protected function setUp() { - $this->product = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); - $this->categoriesCollection = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Category\Collection') - ->disableOriginalConstructor()->getMock(); - $this->product->expects($this->any())->method('getCategoryCollection') + $this->product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $this->categoriesCollection = $this->getMockBuilder( + \Magento\Catalog\Model\ResourceModel\Category\Collection::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->product->expects($this->any()) + ->method('getCategoryCollection') ->will($this->returnValue($this->categoriesCollection)); - $this->storeManager = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') - ->disableOriginalConstructor()->getMock(); - $this->categoriesCollection->expects($this->exactly(2))->method('addAttributeToSelect') - ->will($this->returnSelf()); + $this->storeManager = $this->getMockBuilder( + \Magento\Store\Model\StoreManagerInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->currentUrlRewritesRegenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->categoriesUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->anchorUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); - $this->objectRegistryFactory = $this->getMockBuilder('Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory') - ->disableOriginalConstructor()->setMethods(['create'])->getMock(); - $this->storeViewService = $this->getMockBuilder('Magento\CatalogUrlRewrite\Service\V1\StoreViewService') - ->disableOriginalConstructor()->getMock(); - + \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->objectRegistryFactory = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->storeViewService = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->productScopeRewriteGenerator = $this->getMockBuilder( + ProductScopeRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->productUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::class, [ 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, 'categoriesUrlRewriteGenerator' => $this->categoriesUrlRewriteGenerator, @@ -87,172 +118,43 @@ protected function setUp() ); $reflection = new \ReflectionClass(get_class($this->productUrlRewriteGenerator)); - $reflectionProperty = $reflection->getProperty('anchorUrlRewriteGenerator'); + $reflectionProperty = $reflection->getProperty('productScopeRewriteGenerator'); $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->productUrlRewriteGenerator, $this->anchorUrlRewriteGenerator); - } - - /** - * Test method - */ - public function testGenerationForGlobalScope() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); - $this->product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); - $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') - ->will($this->returnValue(false)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $categories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $categories->setTargetPath('category-2') - ->setStoreId(2); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$categories])); - $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $current->setTargetPath('category-3') - ->setStoreId(3); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$current])); - $anchorCategories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $anchorCategories->setTargetPath('category-4') - ->setStoreId(4); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$anchorCategories])); - - $this->assertEquals( - [ - 'category-1-1' => $canonical, - 'category-2-2' => $categories, - 'category-3-3' => $current, - 'category-4-4' => $anchorCategories - ], - $this->productUrlRewriteGenerator->generate($this->product) - ); - } - - /** - * Test method - */ - public function testGenerationForSpecificStore() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $storeRootCategoryId = 'root-for-store-id'; - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $category->expects($this->any())->method('getParentIds') - ->will($this->returnValue(['root-id', $storeRootCategoryId])); - $category->expects($this->any())->method('getParentId')->will($this->returnValue('parent_id')); - $category->expects($this->any())->method('getId')->will($this->returnValue('category_id')); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue($storeRootCategoryId)); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$category])); - $this->initObjectRegistryFactory([$category]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipRootCategoryForCategoriesGenerator() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue('root-for-store-id')); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $rootCategory = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $rootCategory->expects($this->any())->method('getParentIds')->will($this->returnValue([1, 2])); - $rootCategory->expects($this->any())->method('getParentId')->will($this->returnValue(Category::TREE_ROOT_ID)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$rootCategory])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipGenerationForNotStoreRootCategory() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $category->expects($this->any())->method('getParentIds') - ->will($this->returnValue(['root-id', 'root-for-store-id'])); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue('not-root-id')); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$category])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipGenerationForGlobalScope() - { - $this->product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); - $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') - ->will($this->returnValue(true)); - - $this->assertEquals([], $this->productUrlRewriteGenerator->generate($this->product)); + $reflectionProperty->setValue($this->productUrlRewriteGenerator, $this->productScopeRewriteGenerator); } /** - * @param array $entities + * Covers generate(). + * + * @return void */ - protected function initObjectRegistryFactory($entities) + public function testGenerate() { - $objectRegistry = $this->getMockBuilder('Magento\CatalogUrlRewrite\Model\ObjectRegistry') - ->disableOriginalConstructor()->getMock(); - $this->objectRegistryFactory->expects($this->any())->method('create') - ->with(['entities' => $entities]) - ->will($this->returnValue($objectRegistry)); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $productCategoriesMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $productCategoriesMock->expects($this->exactly(2)) + ->method('addAttributeToSelect') + ->withConsecutive(['url_key'], ['url_path']) + ->willReturnSelf(); + $productMock->expects($this->once()) + ->method('getCategoryCollection') + ->willReturn($productCategoriesMock); + $this->productScopeRewriteGenerator->expects($this->once()) + ->method('generateForSpecificStoreView') + ->willReturn($urls); + $this->assertEquals($urls, $this->productUrlRewriteGenerator->generate($productMock, 1)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php new file mode 100644 index 0000000000000..2e4057f30d258 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php @@ -0,0 +1,44 @@ +urlPersistMock = $this->getMock(UrlPersistInterface::class); + $this->urlRewriteBunchReplacer = new UrlRewriteBunchReplacer( + $this->urlPersistMock + ); + } + + /** + * Covers doBunchReplace() method. + * + * @return void + */ + public function testDoBunchReplace() + { + $urls = [[1], [2]]; + $this->urlPersistMock->expects($this->exactly(2)) + ->method('replace') + ->withConsecutive([[[1]]], [[[2]]]); + $this->urlRewriteBunchReplacer->doBunchReplace($urls, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index acb02d4a644a4..d9f9edd23387c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -23,12 +23,12 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var string */ - protected $categoryId = 10; + private $categoryId = 10; /** * @var \Magento\UrlRewrite\Model\UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlPersist; + private $urlPersist; /** * @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject @@ -38,12 +38,12 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlRewriteGenerator; + private $productUrlRewriteGenerator; /** * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepository; + private $productRepository; /** * @var \Magento\CatalogImportExport\Model\Import\Product|\PHPUnit_Framework_MockObject_MockObject @@ -53,67 +53,69 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject */ - protected $observer; + private $observer; /** * @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ - protected $event; + private $event; /** * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogProductFactory; + private $catalogProductFactory; /** * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + private $storeManager; /** * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistryFactory; + private $objectRegistryFactory; /** * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlPathGenerator; + private $productUrlPathGenerator; /** * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; /** * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistry; + private $objectRegistry; /** - * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver */ - protected $importMock; + private $import; /** - * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $import; + private $product; /** - * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; + private $mergeDataProvider; /** * Test products returned by getBunch method of event object. @@ -147,7 +149,7 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->importProduct = $this->getMock( - '\Magento\CatalogImportExport\Model\Import\Product', + \Magento\CatalogImportExport\Model\Import\Product::class, [ 'getNewSku', 'getProductCategories', @@ -160,7 +162,7 @@ protected function setUp() false ); $this->catalogProductFactory = $this->getMock( - '\Magento\Catalog\Model\ProductFactory', + \Magento\Catalog\Model\ProductFactory::class, [ 'create', ], @@ -170,53 +172,53 @@ protected function setUp() ); $this->storeManager = $this ->getMockBuilder( - '\Magento\Store\Model\StoreManagerInterface' + \Magento\Store\Model\StoreManagerInterface::class ) ->disableOriginalConstructor() ->setMethods([ 'getWebsite', ]) ->getMockForAbstractClass(); - $this->event = $this->getMock('\Magento\Framework\Event', ['getAdapter', 'getBunch'], [], '', false); + $this->event = $this->getMock(\Magento\Framework\Event::class, ['getAdapter', 'getBunch'], [], '', false); $this->event->expects($this->any())->method('getAdapter')->willReturn($this->importProduct); $this->event->expects($this->any())->method('getBunch')->willReturn($this->products); - $this->observer = $this->getMock('\Magento\Framework\Event\Observer', ['getEvent'], [], '', false); + $this->observer = $this->getMock(\Magento\Framework\Event\Observer::class, ['getEvent'], [], '', false); $this->observer->expects($this->any())->method('getEvent')->willReturn($this->event); - $this->urlPersist = $this->getMockBuilder('\Magento\UrlRewrite\Model\UrlPersistInterface') + $this->urlPersist = $this->getMockBuilder(\Magento\UrlRewrite\Model\UrlPersistInterface::class) ->disableOriginalConstructor() ->getMock(); $this->productUrlRewriteGenerator = - $this->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator') + $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::class) ->disableOriginalConstructor() ->setMethods(['generate']) ->getMock(); - $this->productRepository = $this->getMockBuilder('\Magento\Catalog\Api\ProductRepositoryInterface') + $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); $this->objectRegistryFactory = $this->getMock( - '\Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory', + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class, [], [], '', false ); $this->productUrlPathGenerator = $this->getMock( - '\Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class, [], [], '', false ); $this->storeViewService = $this->getMock( - '\Magento\CatalogUrlRewrite\Service\V1\StoreViewService', + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class, [], [], '', false ); $this->urlRewriteFactory = $this->getMock( - '\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory', + \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class, [ 'create', ], @@ -225,7 +227,7 @@ protected function setUp() false ); $this->urlFinder = $this - ->getMockBuilder('\Magento\UrlRewrite\Model\UrlFinderInterface') + ->getMockBuilder(\Magento\UrlRewrite\Model\UrlFinderInterface::class) ->setMethods([ 'findAllByData', ]) @@ -233,22 +235,22 @@ protected function setUp() ->getMockForAbstractClass(); $this->urlRewrite = $this - ->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + ->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor() ->getMock(); $this->product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $this->objectRegistry = $this - ->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ObjectRegistry') + ->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) ->disableOriginalConstructor() ->getMock(); $categoryProcessor = $this->getMock( - '\Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor', + \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor::class, [ 'getCategoryById', ], @@ -257,7 +259,7 @@ protected function setUp() false ); $category = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'getId', ], @@ -278,10 +280,19 @@ protected function setUp() ->expects($this->any()) ->method('getCategoryProcessor') ->willReturn($categoryProcessor); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->objectManager = new ObjectManager($this); $this->import = $this->objectManager->getObject( - '\Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver', + \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::class, [ 'catalogProductFactory' => $this->catalogProductFactory, 'objectRegistryFactory' => $this->objectRegistryFactory, @@ -291,13 +302,14 @@ protected function setUp() 'urlPersist' => $this->urlPersist, 'urlRewriteFactory' => $this->urlRewriteFactory, 'urlFinder' => $this->urlFinder, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } /** - * Test for afterImportData() - * Covers afterImportData() + protected methods used inside + * Test for afterImportData(). + * Covers afterImportData() + protected methods used inside. * * @covers \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::_populateForUrlGeneration * @covers \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::isGlobalScope @@ -312,7 +324,7 @@ public function testAfterImportData() $websiteId = 'websiteId value'; $productsCount = count($this->products); $websiteMock = $this->getMock( - '\Magento\Store\Model\Website', + \Magento\Store\Model\Website::class, [ 'getStoreIds', ], @@ -364,7 +376,7 @@ public function testAfterImportData() ->method('getStoreIdByCode') ->will($this->returnValueMap($map)); $product = $this->getMock( - '\Magento\Catalog\Model\Product', + \Magento\Catalog\Model\Product::class, [ 'getId', 'setId', @@ -433,14 +445,15 @@ public function testAfterImportData() $this->urlRewrite->expects($this->any())->method('setRequestPath')->willReturnSelf(); $this->urlRewrite->expects($this->any())->method('setTargetPath')->willReturnSelf(); $this->urlRewrite->expects($this->any())->method('getTargetPath')->willReturn('targetPath'); + $this->urlRewrite->expects($this->any())->method('getRequestPath')->willReturn('requestPath'); $this->urlRewrite->expects($this->any())->method('getStoreId') ->willReturnOnConsecutiveCalls(0, 'not global'); $this->urlRewriteFactory->expects($this->any())->method('create')->willReturn($this->urlRewrite); $productUrls = [ - 'targetPath-0' => $this->urlRewrite, - 'targetPath-not global' => $this->urlRewrite + 'requestPath_0' => $this->urlRewrite, + 'requestPath_not global' => $this->urlRewrite ]; $this->urlPersist @@ -452,7 +465,7 @@ public function testAfterImportData() } /** - * Cover canonicalUrlRewriteGenerate(). + * Covers canonicalUrlRewriteGenerate(). */ public function testCanonicalUrlRewriteGenerateWithUrlPath() { @@ -460,7 +473,7 @@ public function testCanonicalUrlRewriteGenerateWithUrlPath() $requestPath = 'simple-product.html'; $storeId = 10; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [$storeId => $product]; @@ -523,14 +536,14 @@ public function testCanonicalUrlRewriteGenerateWithUrlPath() } /** - * Cover canonicalUrlRewriteGenerate(). + * Covers canonicalUrlRewriteGenerate(). */ public function testCanonicalUrlRewriteGenerateWithEmptyUrlPath() { $productId = 'product_id'; $storeId = 10; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [$storeId => $product]; @@ -553,7 +566,7 @@ public function testCanonicalUrlRewriteGenerateWithEmptyUrlPath() } /** - * Cover categoriesUrlRewriteGenerate(). + * Covers categoriesUrlRewriteGenerate(). */ public function testCategoriesUrlRewriteGenerate() { @@ -562,7 +575,7 @@ public function testCategoriesUrlRewriteGenerate() $productId = 'product_id'; $canonicalUrlPathWithCategory = 'canonical-path-with-category'; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [ @@ -587,7 +600,7 @@ public function testCategoriesUrlRewriteGenerate() ->expects($this->any()) ->method('getCanonicalUrlPath') ->will($this->returnValue($canonicalUrlPathWithCategory)); - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); + $category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); $category ->expects($this->any()) ->method('getId') @@ -715,7 +728,7 @@ protected function currentUrlRewritesRegeneratorGetCurrentRewritesMocks($current /** * @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php new file mode 100644 index 0000000000000..b90a67bcda2b3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -0,0 +1,321 @@ +categoryUrlRewriteGeneratorMock = $this->getMock( + CategoryUrlRewriteGenerator::class, + [ + 'generate' + ], + [], + '', + false + ); + $this->urlPersist = $this->getMock(UrlPersistInterface::class, [], [], '', false); + $this->urlRewriteHandler = $this->getMock( + UrlRewriteHandler::class, + [ + 'generateProductUrlRewrites', + 'deleteCategoryRewritesForChildren', + ], + [], + '', + false + ); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->setMethods(['isSetFlag']) + ->getMockForAbstractClass(); + + $this->urlRewriteBunchReplacerMock = $this->getMock( + UrlRewriteBunchReplacer::class, + ['doBunchReplace'], + [], + '', + false + ); + + $this->objectManager = new ObjectManager($this); + $this->observerModel = $this->objectManager->getObject( + CategoryProcessUrlRewriteMovingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlPersist' => $this->urlPersist, + 'scopeConfig' => $this->scopeConfig, + 'urlRewriteHandler' => $this->urlRewriteHandler + ] + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->observerModel, + 'urlRewriteBunchReplacer', + $this->urlRewriteBunchReplacerMock + ); + } + + /** + * Covers execite() method. + * + * @dataProvider testExecuteDataProvider + * @return void + */ + public function testExecute($changedParent, $saveRewritesHistory) + { + $storeId = 1; + $category = $this->getMock( + \Magento\Catalog\Model\Category::class, + [ + 'getStoreId', + 'dataHasChangedFor' + ], + [], + '', + false + ); + $category->expects($this->once())->method('dataHasChangedFor')->with('parent_id')->willReturn($changedParent); + + if ($changedParent) { + $this->getMockData($saveRewritesHistory, $category, $storeId); + } + + $event = $this->getMock(\Magento\Framework\Event::class, ['getCategory'], [], '', false); + $event->expects($this->any())->method('getCategory')->willReturn($category); + + $observer = $this->getMock( + \Magento\Framework\Event\Observer::class, + ['getEvent'], + [], + '', + false + ); + $observer->expects($this->any())->method('getEvent')->willReturn($event); + $this->observerModel->execute($observer); + } + + /** + * Data provider for testExecute() + * + * @return array + */ + public function testExecuteDataProvider() + { + return [ + 1 => [ + 'changedParent' => true, + 'saveRewritesHistory' => true + ], + 2 => [ + 'changedParent' =>false, + 'saveRewritesHistory' => true + ], + ]; + } + + /** + * Returns Mock data for test. + * + * @param $saveRewritesHistory + * @param $category + * @param $storeId + */ + private function getMockData($saveRewritesHistory, $category, $storeId) + { + $category->expects($this->once())->method('getStoreId')->willReturn($storeId); + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with( + UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + )->willReturn($saveRewritesHistory); + + $this->categoryUrlRewriteGeneratorMock + ->expects($this->once()) + ->method('generate') + ->with($category, true) + ->willReturn($this->getCategoryUrlRewritesGenerated()); + + $this->urlRewriteHandler + ->expects($this->once()) + ->method('generateProductUrlRewrites') + ->with($category) + ->willReturn($this->getProductUrlRewritesGenerated()); + $this->urlRewriteHandler + ->expects($this->once()) + ->method('deleteCategoryRewritesForChildren') + ->with($category) + ->willReturn(null); + + $this->urlRewriteBunchReplacerMock->expects($this->once()) + ->method('doBunchReplace'); + } + + /** + * Returns an array of generated UrlRewrites for category. + * + * @return array + */ + private function getCategoryUrlRewritesGenerated() + { + $categoryUrlRewriteGenerated1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteGenerated1->setRequestPath('category1/category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('catalog/category/view/id/6'); + $categoryUrlRewriteGenerated2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteGenerated2->setRequestPath('category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('category1/category2.html') + ->setRedirectType(301) + ->setIsAutogenerated(0); + + $categoryUrlRewriteGenerated = [ + 'category1/category2.html_1' => $categoryUrlRewriteGenerated1, + 'category2.html_1' => $categoryUrlRewriteGenerated2 + ]; + + return $categoryUrlRewriteGenerated; + } + + /** + * Returns an array of generated UrlRewrites for products. + * + * @return array + */ + private function getProductUrlRewritesGenerated() + { + $productUrlRewriteGenerated1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteGenerated2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category1/category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated3 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated3->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('category1/category2/simple1.html') + ->setRedirectType(301) + ->setStoreId(1) + ->setDescription(null) + ->setIsAutogenerated(0) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated4 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated4->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category1/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/5') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"5";}'); + $productUrlRewriteGenerated5 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated5->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('simple2.html') + ->setTargetPath('catalog/product/view/id/2') + ->setStoreId(1); + $productUrlRewriteGenerated6 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated6->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category1/category2/simple2.html') + ->setTargetPath('catalog/product/view/id/2/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated7 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated7->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category2/simple2.html') + ->setTargetPath('category1/category2/simple2.html') + ->setRedirectType(301) + ->setStoreId(1) + ->setDescription(null) + ->setIsAutogenerated(0) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteGenerated8 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated8->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category1/simple2.html') + ->setTargetPath('catalog/product/view/id/2/category/5') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteGenerated = [ + 'simple1.html_1' => $productUrlRewriteGenerated1, + 'category1/category2/simple1.html_1' => $productUrlRewriteGenerated2, + 'category2/simple1.html_1' => $productUrlRewriteGenerated3, + 'category1/simple1.html_1' => $productUrlRewriteGenerated4, + 'simple2.html_1' => $productUrlRewriteGenerated5, + 'category1/category2/simple2.html_1' => $productUrlRewriteGenerated6, + 'category2/simple2.html_1' => $productUrlRewriteGenerated7, + 'category1/simple2.html_1' => $productUrlRewriteGenerated8 + ]; + + return $productUrlRewriteGenerated; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php new file mode 100644 index 0000000000000..f22417c1be1e8 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php @@ -0,0 +1,261 @@ +categoryUrlRewriteGeneratorMock = $this->getMock( + CategoryUrlRewriteGenerator::class, + ['generate'], + [], + '', + false + ); + $this->urlRewriteHandlerMock = $this->getMock( + UrlRewriteHandler::class, + ['generateProductUrlRewrites'], + [], + '', + false + ); + $this->urlRewriteBunchReplacerMock = $this->getMock( + UrlRewriteBunchReplacer::class, + ['doBunchReplace'], + [], + '', + false + ); + $this->databaseMapPoolMock = $this->getMock(DatabaseMapPool::class, ['resetMap'], [], '', false); + + $this->objectManager = new ObjectManager($this); + $this->observerModel = $this->objectManager->getObject( + CategoryProcessUrlRewriteSavingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlRewriteHandler' => $this->urlRewriteHandlerMock, + 'urlRewriteBunchReplacer' => $this->urlRewriteBunchReplacerMock, + 'databaseMapPool' => $this->databaseMapPoolMock, + 'dataUrlRewriteClassNames' => $this->dataUrlRewriteClassNames + ] + ); + } + + /** + * Covers execite() method. + * + * @dataProvider executeDataProvider + * @return void + */ + public function testExecute( + $parentId, + $isChangedUrlKey, + $isChangedIsAnchor, + $isChangedProductList + ) { + $categoryId = 6; + /** @var \PHPUnit_Framework_MockObject_MockObject $category */ + $category = $this->getMock( + \Magento\Catalog\Model\Category::class, + [ + 'getEntityId', + 'getParentId', + 'dataHasChangedFor', + 'getIsChangedProductList' + ], + [], + '', + false + ); + $category->expects($this->any())->method('getEntityId')->willReturn($categoryId); + $category->expects($this->once())->method('getParentId')->willReturn($parentId); + $category->expects($this->any())->method('dataHasChangedFor') + ->willReturnMap( + [ + ['url_key', $isChangedUrlKey], + ['is_anchor', $isChangedIsAnchor] + ] + ); + $category->expects($this->any())->method('getIsChangedProductList')->willReturn($isChangedProductList); + + $categoryUrlRewriteResult = $this->getCategoryUrlRewriteResult(); + $this->categoryUrlRewriteGeneratorMock + ->expects($this->any()) + ->method('generate') + ->with($category) + ->willReturn($categoryUrlRewriteResult); + $productUrlRewriteResult = $this->getProductUrlRewriteResult(); + $this->urlRewriteHandlerMock + ->expects($this->any()) + ->method('generateProductUrlRewrites') + ->with($category) + ->willReturn($productUrlRewriteResult); + $this->urlRewriteBunchReplacerMock + ->expects($this->any()) + ->method('doBunchReplace'); + $this->databaseMapPoolMock + ->expects($this->any()) + ->method('resetMap'); + + $event = $this->getMock(\Magento\Framework\Event::class, ['getData'], [], '', false); + $event->expects($this->any())->method('getData')->with('category')->willReturn($category); + $observer = $this->getMock( + \Magento\Framework\Event\Observer::class, + ['getEvent'], + [], + '', + false + ); + $observer->expects($this->any())->method('getEvent')->willReturn($event); + + $this->observerModel->execute($observer); + } + + /** + * Data provider for testExecute(). + * + * @return array + */ + public function executeDataProvider() + { + return [ + 'tree_root_category_parent' => [ + 'parentId' => \Magento\Catalog\Model\Category::TREE_ROOT_ID, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'true_category_data' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'false_category_data' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'true_isChangedUrlKey' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => false, + 'isChangedProductList' => false, + ], + 'true_isChangedIsAnchor' => [ + 'parentId' => 3, + 'isChangedUrlKey' => false, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => false, + ], + 'true_isChangedProductList' => [ + 'parentId' => 3, + 'isChangedUrlKey' => false, + 'isChangedIsAnchor' => false, + 'isChangedProductList' => true, + ], + ]; + } + + /** + * Returns category urlRewrite result. + * + * @return array + */ + private function getCategoryUrlRewriteResult() + { + $categoryUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteResult1->setRequestPath('category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('catalog/category/view/id/6'); + $categoryUrlRewriteResult = [ + 'category2.html_1' => $categoryUrlRewriteResult1, + ]; + + return $categoryUrlRewriteResult; + } + + /** + * Returns products urlRewrite result. + * + * @return array + */ + private function getProductUrlRewriteResult() + { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteResult2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + 'category2/simple1.html_1' => $productUrlRewriteResult2, + ]; + + return $productUrlRewriteResult; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php new file mode 100644 index 0000000000000..c54a1a8bf9f2b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php @@ -0,0 +1,394 @@ +categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + ->setMethods( + [ + 'getAffectedProductIds', + 'getData', + 'getStoreId', + 'getEntityId', + 'getId', + 'getProductCollection' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $this->childrenCategoriesProviderMock = $this->getMockBuilder(ChildrenCategoriesProvider::class) + ->getMock(); + $this->categoryUrlRewriteGeneratorMock = $this->getMockBuilder(CategoryUrlRewriteGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productUrlRewriteGeneratorMock = $this->getMockBuilder(ProductUrlRewriteGenerator::class) + ->setMethods(['generate']) + ->disableOriginalConstructor() + ->getMock(); + $this->urlPersistMock = $this->getMockBuilder(UrlPersistInterface::class) + ->getMock(); + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderFactoryMock = $this->getMockBuilder(MergeDataProviderFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderMock = $this->getMockBuilder(MergeDataProvider::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->mergeDataProviderMock); + + $this->urlRewriteHandler = new UrlRewriteHandler( + $this->childrenCategoriesProviderMock, + $this->categoryUrlRewriteGeneratorMock, + $this->productUrlRewriteGeneratorMock, + $this->urlPersistMock, + $this->collectionFactoryMock, + $this->mergeDataProviderFactoryMock + ); + + $this->categoryBasedProductRewriteGeneratorMock = $this->getMockBuilder( + CategoryBasedProductRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManager = new ObjectManager($this); + $this->objectManager->setBackwardCompatibleProperty( + $this->urlRewriteHandler, + 'categoryBasedProductRewriteGenerator', + $this->categoryBasedProductRewriteGeneratorMock + ); + + $this->productItem = $this->getMock( + \Magento\Catalog\Model\Product::class, + [ + 'getId', + 'setStoreId', + 'setData' + ], + [], + '', + false + ); + } + + public function testDeleteCategoryRewritesForChildren() + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(2); + + $this->childrenCategoriesProviderMock->expects($this->once()) + ->method('getChildrenIds') + ->with($this->categoryMock, true) + ->willReturn([3, 4]); + + $this->urlRewriteHandler->deleteCategoryRewritesForChildren($this->categoryMock); + } + + /** + * Covers generateProductUrlRewrites(), getCategoryProductsUrlRewrites() methods. + * + * @dataProvider generateProductUrlRewritesDataProvider + * @return void + */ + public function testGenerateProductUrlRewrites($affectedProductIds) + { + $storeId = 0; + $saveRewritesHistory = true; + $categoryId = 6; + $this->categoryMock->expects($this->once()) + ->method('getData') + ->with('save_rewrites_history') + ->willReturn($saveRewritesHistory); + $this->categoryMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->categoryMock->expects($this->any()) + ->method('getAffectedProductIds') + ->willReturn($affectedProductIds); + $this->categoryMock->expects($this->once()) + ->method('getEntityId') + ->willReturn($categoryId); + + if ($affectedProductIds) { + $this->callIfAffectedProductsIsset($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds); + } else { + $this->getCategoryProductsUrlRewrites($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds); + } + + $this->childrenCategoriesProviderMock->expects($this->once()) + ->method('getChildren') + ->with($this->categoryMock, true) + ->willReturn([]); + $generatedUrlRewrites = $this->getProductUrlRewriteResult($affectedProductIds); + $this->mergeDataProviderMock->expects($this->any()) + ->method('getData') + ->willReturn($generatedUrlRewrites); + + $this->assertEquals( + $generatedUrlRewrites, + $this->urlRewriteHandler->generateProductUrlRewrites($this->categoryMock) + ); + } + + /** + * Calls when $affectedProductIds is not empty. + * + * @param $saveRewritesHistory + * @param $storeId + * @param $categoryId + * @param $affectedProductIds + * @return void + */ + private function callIfAffectedProductsIsset($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds) + { + $productCollectionMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) + ->setMethods( + [ + 'getData', + 'setStoreId', + 'addIdFilter', + 'addAttributeToSelect' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $productCollectionMock->expects($this->once()) + ->method('setStoreId') + ->with($storeId) + ->willReturn($productCollectionMock); + $productCollectionMock->expects($this->once()) + ->method('addIdFilter') + ->with($affectedProductIds) + ->willReturn($productCollectionMock); + $productCollectionMock = $this->setAdditionalMocks($productCollectionMock, $storeId, $saveRewritesHistory); + $this->collectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($productCollectionMock); + $this->productUrlRewriteGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->productItem, $categoryId) + ->willReturn($this->getProductUrlRewriteResult($affectedProductIds)); + } + + /** + * Calls when $affectedProductIds is empty. + * + * @param $saveRewritesHistory + * @param $storeId + * @param $categoryId + * @param $affectedProductIds + * @return void + */ + private function getCategoryProductsUrlRewrites($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds) + { + $productCollection = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\Collection::class, + ['addAttributeToSelect'], + [], + '', + false + ); + $productCollection = $this->setAdditionalMocks($productCollection, $storeId, $saveRewritesHistory); + $this->categoryMock->expects($this->once()) + ->method('getProductCollection') + ->willReturn($productCollection); + $categoryBasedProductRewriteGenerated = $this->getProductUrlRewriteResult($affectedProductIds); + $this->categoryBasedProductRewriteGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->productItem, $this->categoryMock, $categoryId) + ->willReturn($categoryBasedProductRewriteGenerated); + $this->productItem->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + } + + /** + * DataProvider for testGenerateProductUrlRewrites(). + * + * @return array + */ + public function generateProductUrlRewritesDataProvider() + { + return [ + 1 => [ + 'affectedProductIds' => null + ], + 2 => [ + 'affectedProductIds' => [0 => 2] + ] + ]; + } + + /** + * Returns products urlRewrite result. + * + * @return array + */ + private function getProductUrlRewriteResult($affectedProductIds) + { + if ($affectedProductIds) { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('simple2.html') + ->setTargetPath('catalog/product/view/id/2') + ->setStoreId(1); + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + ]; + } else { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteResult2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + 'category2/simple1.html_1' => $productUrlRewriteResult2, + ]; + } + + return $productUrlRewriteResult; + } + + /** + * Sets additional data to the product Collection Mock. + * + * @param $productCollectionMock + * @return $productCollectionMock + */ + private function setAdditionalMocks($productCollectionMock, $storeId, $saveRewritesHistory) + { + $productCollectionMock->expects($this->any())->method('addAttributeToSelect') + ->willReturnMap( + [ + ['visibility', false, $productCollectionMock], + ['name', false, $productCollectionMock], + ['url_key', false, $productCollectionMock], + ['url_path', false, $productCollectionMock] + ] + ); + $this->productItem->expects($this->once()) + ->method('setStoreId') + ->with($storeId) + ->willReturn($this->productItem); + $this->productItem->expects($this->once()) + ->method('setData') + ->with('save_rewrites_history', $saveRewritesHistory) + ->willReturn($this->productItem); + $this->objectManager->setBackwardCompatibleProperty( + $productCollectionMock, + '_items', + [$this->productItem] + ); + $this->objectManager->setBackwardCompatibleProperty( + $productCollectionMock, + '_isCollectionLoaded', + true + ); + + return $productCollectionMock; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index 5a8974fc52da4..7f0fc718083ec 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -23,4 +23,12 @@ + + + + Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap + Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap + + + diff --git a/app/code/Magento/UrlRewrite/Model/MergeDataProvider.php b/app/code/Magento/UrlRewrite/Model/MergeDataProvider.php new file mode 100644 index 0000000000000..6131c5f9ad647 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Model/MergeDataProvider.php @@ -0,0 +1,56 @@ +getRequestPath() . self::SEPARATOR . $urlRewrite->getStoreId(); + if ($key !== self::SEPARATOR) { + $this->data[$key] = $urlRewrite; + } else { + $this->data[] = $urlRewrite; + } + } + } + + /** + * Returns the data added to container. + * + * @return UrlRewriteService[] + */ + public function getData() + { + return $this->data; + } +} diff --git a/app/code/Magento/UrlRewrite/Test/Unit/Model/MergeDataProviderTest.php b/app/code/Magento/UrlRewrite/Test/Unit/Model/MergeDataProviderTest.php new file mode 100644 index 0000000000000..577eaff07605a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Unit/Model/MergeDataProviderTest.php @@ -0,0 +1,127 @@ +urlRewritesSet = (new ObjectManager($this))->getObject( + MergeDataProvider::class, + [] + ); + } + + /** + * Run test merge method. + * + * @param array $urlRewriteMockArray + * @param String $expectedData + * @param int $arrayCount + * @dataProvider mergeDataProvider + * @return void + */ + public function testMerge($urlRewriteMockArray, $expectedData, $arrayCount) + { + $this->urlRewritesSet->merge($urlRewriteMockArray); + $this->assertEquals($expectedData, $this->urlRewritesSet->getData()); + $this->assertCount($arrayCount, $this->urlRewritesSet->getData()); + } + + /** + * Run test getData() method when data is Empty + * + * @return void + */ + public function testGetDataWhenEmpty() + { + $this->assertEmpty($this->urlRewritesSet->getData()); + } + + /** + * Data provider for testMerge + * + * @return array + */ + public function mergeDataProvider() + { + $urlRewriteMock1 = $this->getMock(UrlRewrite::class, [], [], '', false); + + $requestPathForMock2 = 'magento.tst/products/simpleproduct2'; + $storeIdForMock2 = 'testStore2'; + $urlRewriteMock2 = $this->getMock(UrlRewrite::class, [], [], '', false); + + $urlRewriteMock2->expects($this->atLeastOnce()) + ->method('getRequestPath') + ->willReturn($requestPathForMock2); + + $urlRewriteMock2->expects($this->atLeastOnce()) + ->method('getStoreId') + ->willReturn($storeIdForMock2); + + $requestPathForMock3 = 'magento.tst/products/simpleproduct3'; + $storeIdForMock3 = 'testStore3'; + $urlRewriteMock3 = $this->getMock(UrlRewrite::class, [], [], '', false); + + $urlRewriteMock3->expects($this->atLeastOnce()) + ->method('getRequestPath') + ->willReturn($requestPathForMock3); + + $urlRewriteMock3->expects($this->atLeastOnce()) + ->method('getStoreId') + ->willReturn($storeIdForMock3); + + return [ + [ + [], + [], + 0, + ], + [ + [$urlRewriteMock1], + [$urlRewriteMock1], + 1, + ], + [ + [ + $urlRewriteMock1, + $urlRewriteMock2, + $urlRewriteMock2, + ], + [ + $urlRewriteMock1, + $requestPathForMock2 . '_' . $storeIdForMock2 => $urlRewriteMock2, + ], + 2, + ], + [ + [ + $urlRewriteMock1, + $urlRewriteMock2, + $urlRewriteMock3, + ], + [ + $urlRewriteMock1, + $requestPathForMock2 . '_' . $storeIdForMock2 => $urlRewriteMock2, + $requestPathForMock3 . '_' . $storeIdForMock3 => $urlRewriteMock3, + ], + 3, + ], + ]; + } +} diff --git a/app/etc/di.xml b/app/etc/di.xml index d180f173737c9..279d5b9b93942 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1208,4 +1208,17 @@ + + + + HASH + BTREE + + + INNODB + MEMORY + MYISAM + + + diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserverTest.php new file mode 100644 index 0000000000000..5354ad92a362d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserverTest.php @@ -0,0 +1,186 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @param array $filter + * @return array + */ + private function getActualResults(array $filter) + { + /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $actualResults = []; + foreach ($urlFinder->findAllByData($filter) as $url) { + $actualResults[] = [ + 'request_path' => $url->getRequestPath(), + 'target_path' => $url->getTargetPath(), + 'is_auto_generated' => (int)$url->getIsAutogenerated(), + 'redirect_type' => $url->getRedirectType(), + ]; + } + + return $actualResults; + } + + public function tearDown() + { + $category = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + $category->load(3); + $category->delete(); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testUrlKeyHasChanged() + { + $categoryFilter = [ + UrlRewrite::ENTITY_TYPE => 'category', + ]; + $expected = [ + [ + 'request_path' => "category-1.html", + 'target_path' => "catalog/category/view/id/3", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-1/category-1-1.html", + 'target_path' => "catalog/category/view/id/4", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-1/category-1-1/category-1-1-1.html", + 'target_path' => "catalog/category/view/id/5", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-2.html", + 'target_path' => "catalog/category/view/id/6", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ] + ]; + $actual = $this->getActualResults($categoryFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + /** @var \Magento\Catalog\Model\Category $category */ + $category = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + $category->load(3); + $category->setData('save_rewrites_history', false); + $category->setUrlKey('new-url'); + $category->save(); + $expected = [ + [ + 'request_path' => "new-url.html", + 'target_path' => "catalog/category/view/id/3", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "new-url/category-1-1.html", + 'target_path' => "catalog/category/view/id/4", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "new-url/category-1-1/category-1-1-1.html", + 'target_path' => "catalog/category/view/id/5", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-2.html", + 'target_path' => "catalog/category/view/id/6", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ] + ]; + $actual = $this->getActualResults($categoryFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testIsAnchorHasChanged() + { + $categoryFilter = [ + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + ]; + /** @var \Magento\Catalog\Model\Category $category */ + $category = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + $category->load(3); + $category->setData('is_anchor', false); + $category->save(); + $expected = [ + [ + 'request_path' => "category-1.html", + 'target_path' => "catalog/category/view/id/3", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-1/category-1-1.html", + 'target_path' => "catalog/category/view/id/4", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-1/category-1-1/category-1-1-1.html", + 'target_path' => "catalog/category/view/id/5", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ], + [ + 'request_path' => "category-2.html", + 'target_path' => "catalog/category/view/id/6", + 'is_auto_generated' => 1, + 'redirect_type' => 0, + ] + ]; + $actual = $this->getActualResults($categoryFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php index a8ef658b92f01..fce648b8316c8 100644 --- a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php @@ -40,6 +40,13 @@ class AuthSessionTest extends \PHPUnit_Framework_TestCase */ protected $dateTime; + /** + * Session Manager. + * + * @var \Magento\Framework\Session\SessionManager + */ + private $sessionManager; + /** * Set up */ @@ -48,14 +55,15 @@ protected function setUp() parent::setUp(); $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->objectManager->get('Magento\Framework\Config\ScopeInterface') + $this->sessionManager = $this->objectManager->create(\Magento\Framework\Session\SessionManager::class); + $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); - $this->auth = $this->objectManager->create('Magento\Backend\Model\Auth'); - $this->authSession = $this->objectManager->create('Magento\Backend\Model\Auth\Session'); - $this->adminSessionInfo = $this->objectManager->create('Magento\Security\Model\AdminSessionInfo'); + $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); + $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->adminSessionInfo = $this->objectManager->create(\Magento\Security\Model\AdminSessionInfo::class); $this->auth->setAuthStorage($this->authSession); - $this->adminSessionsManager = $this->objectManager->create('Magento\Security\Model\AdminSessionsManager'); - $this->dateTime = $this->objectManager->create('Magento\Framework\Stdlib\DateTime'); + $this->adminSessionsManager = $this->objectManager->create(\Magento\Security\Model\AdminSessionsManager::class); + $this->dateTime = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime::class); } /** diff --git a/lib/internal/Magento/Framework/DB/TemporaryTableService.php b/lib/internal/Magento/Framework/DB/TemporaryTableService.php new file mode 100644 index 0000000000000..94a66713d57f0 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/TemporaryTableService.php @@ -0,0 +1,179 @@ +random = $random; + $this->allowedIndexMethods = $allowedIndexMethods; + $this->allowedEngines = $allowedEngines; + } + + /** + * Creates a temporary table from select removing duplicate rows if you have a union in your select. + * This method should always be paired with dropTable to ensure cleanup. + * Make sure you index your data so you can query it fast. + * You can choose from memory or file table and provide indexes to ensure fast data query. + * + * Example: createFromSelect( + * $selectObject, + * $this->resourceConnection->getConnection(), + * [ + * 'PRIMARY' => ['primary_id'], + * 'some_single_field_index' => ['field'], + * 'UNQ_some_multiple_field_index' => ['field1', 'field2'], + * ] + * ) + * Note that indexes names with UNQ_ prefix, will be created as unique. + * + * @param Select $select + * @param AdapterInterface $adapter + * @param array $indexes + * @param string $indexMethod + * @param string $dbEngine + * @return string + * @throws \InvalidArgumentException + */ + public function createFromSelect( + Select $select, + AdapterInterface $adapter, + array $indexes = [], + $indexMethod = self::INDEX_METHOD_HASH, + $dbEngine = self::DB_ENGINE_INNODB + ) { + if (!in_array($indexMethod, $this->allowedIndexMethods)) { + throw new \InvalidArgumentException( + sprintf('indexMethod must be one of %s', implode(',', $this->allowedIndexMethods)) + ); + } + + if (!in_array($dbEngine, $this->allowedEngines)) { + throw new \InvalidArgumentException( + sprintf('dbEngine must be one of %s', implode(',', $this->allowedEngines)) + ); + } + + $name = $this->random->getUniqueHash('tmp_select_'); + + $indexStatements = []; + foreach ($indexes as $indexName => $columns) { + $renderedColumns = implode(',', array_map([$adapter, 'quoteIdentifier'], $columns)); + + $indexType = sprintf( + 'INDEX %s USING %s', + $adapter->quoteIdentifier($indexName), + $indexMethod + ); + + if ($indexName === 'PRIMARY') { + $indexType = 'PRIMARY KEY'; + } elseif (strpos($indexName, 'UNQ_') === 0) { + $indexType = sprintf('UNIQUE %s', $adapter->quoteIdentifier($indexName)); + } + + $indexStatements[] = sprintf('%s(%s)', $indexType, $renderedColumns); + } + + $statement = sprintf( + 'CREATE TEMPORARY TABLE %s %s ENGINE=%s IGNORE (%s)', + $adapter->quoteIdentifier($name), + $indexStatements ? '(' . implode(',', $indexStatements) . ')' : '', + $adapter->quoteIdentifier($dbEngine), + "{$select}" + ); + + $adapter->query( + $statement, + $select->getBind() + ); + + $this->createdTableAdapters[$name] = $adapter; + + return $name; + } + + /** + * Method used to drop a table by name. + * This class will hold all temporary table names in createdTableAdapters array + * so we can dispose them once we're finished. + * + * Example: dropTable($name) + * where $name is a variable that holds the name for a previously created temporary table + * by using "createFromSelect" method. + * + * @param string $name + * @return bool + */ + public function dropTable($name) + { + if (!empty($this->createdTableAdapters)) { + if (isset($this->createdTableAdapters[$name]) && !empty($name)) { + $adapter = $this->createdTableAdapters[$name]; + $adapter->dropTemporaryTable($name); + unset($this->createdTableAdapters[$name]); + + return true; + } + } + + return false; + } +} diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php new file mode 100644 index 0000000000000..eb87d7f0a39b7 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php @@ -0,0 +1,211 @@ +adapterMock = $this->getMock(AdapterInterface::class); + $this->selectMock = $this->getMock(Select::class, [], [], '', false); + $this->randomMock = $this->getMock(Random::class, [], [], '', false); + $this->temporaryTableService = (new ObjectManager($this))->getObject( + TemporaryTableService::class, + [ + 'random' => $this->randomMock, + 'allowedIndexMethods' => ['HASH'], + 'allowedEngines' => ['INNODB'], + ] + ); + } + + /** + * Covers createFromSelect() method. + * + * @return void + */ + public function testCreateFromSelectWithException() + { + $this->setExpectedException(\InvalidArgumentException::class); + $random = 'random_table'; + $indexes = [ + ['PRIMARY' => ['primary_column_name']], + 'CREATE TEMPORARY TABLE random_table (PRIMARY KEY(primary_column_name)) ENGINE=INNODB IGNORE ' + . '(select * from sometable)', + ]; + + $this->assertEquals( + $random, + $this->temporaryTableService->createFromSelect( + $this->selectMock, + $this->adapterMock, + $indexes, + TemporaryTableService::INDEX_METHOD_HASH . "Other", + TemporaryTableService::DB_ENGINE_INNODB . "Other" + ) + ); + } + + /** + * Covers createFromSelect() method. + * + * @param array $indexes + * @param string $expectedSelect + * @dataProvider createFromSelectDataProvider + * @return void + */ + public function testCreateFromSelect($indexes, $expectedSelect) + { + $selectString = 'select * from sometable'; + $random = 'random_table'; + + $this->randomMock->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($random); + + $this->adapterMock->expects($this->once()) + ->method('query') + ->with($expectedSelect) + ->willReturnSelf(); + + $this->adapterMock->expects($this->once()) + ->method('query') + ->willReturnSelf(); + + $this->adapterMock->expects($this->any()) + ->method('quoteIdentifier') + ->willReturnArgument(0); + + $this->selectMock->expects($this->once()) + ->method('getBind') + ->willReturn(['bind']); + + $this->selectMock->expects($this->any()) + ->method('__toString') + ->willReturn($selectString); + + $this->assertEquals( + $random, + $this->temporaryTableService->createFromSelect( + $this->selectMock, + $this->adapterMock, + $indexes + ) + ); + } + + /** + * Covers dropTable() method when createdTables array of TemporaryTableService is empty. + * + * @return void + */ + public function testDropTableWhenCreatedTablesArrayIsEmpty() + { + $this->assertFalse($this->temporaryTableService->dropTable('tmp_select_table')); + } + + /** + * Covers dropTable() method when data exists in createdTables array of TemporaryTableService. + * + * @param string $tableName + * @param bool $assertion + * + * @dataProvider dropTableWhenCreatedTablesArrayNotEmptyDataProvider + * @return void + */ + public function testDropTableWhenCreatedTablesArrayNotEmpty($tableName, $assertion) + { + $createdTableAdapters = new \ReflectionProperty($this->temporaryTableService, 'createdTableAdapters'); + $createdTableAdapters->setAccessible(true); + $createdTableAdapters->setValue($this->temporaryTableService, ['tmp_select_table' => $this->adapterMock]); + $createdTableAdapters->setAccessible(false); + + $this->adapterMock->expects($this->any()) + ->method('dropTemporaryTable') + ->willReturn(true); + + $this->assertEquals($this->temporaryTableService->dropTable($tableName), $assertion); + } + + /** + * Data provider for testCreateFromSelect(). + * + * @return array + */ + public function createFromSelectDataProvider() + { + return [ + [ + ['PRIMARY' => ['primary_column_name']], + 'CREATE TEMPORARY TABLE random_table (PRIMARY KEY(primary_column_name)) ENGINE=INNODB IGNORE ' + . '(select * from sometable)', + ], + [ + ['UNQ_INDX' => ['column1', 'column2']], + 'CREATE TEMPORARY TABLE random_table (UNIQUE UNQ_INDX(column1,column2)) ENGINE=INNODB IGNORE ' + . '(select * from sometable)', + ], + [ + ['OTH_INDX' => ['column3', 'column4']], + 'CREATE TEMPORARY TABLE random_table (INDEX OTH_INDX USING HASH(column3,column4)) ENGINE=INNODB IGNORE ' + . '(select * from sometable)', + ], + [ + [ + 'PRIMARY' => ['primary_column_name'], + 'OTH_INDX' => ['column3', 'column4'], + 'UNQ_INDX' => ['column1', 'column2'], + ], + 'CREATE TEMPORARY TABLE random_table ' + . '(PRIMARY KEY(primary_column_name),' + . 'INDEX OTH_INDX USING HASH(column3,column4),UNIQUE UNQ_INDX(column1,column2)) ENGINE=INNODB IGNORE ' + . '(select * from sometable)', + ], + ]; + } + + /** + * Data provider for testDropTableWhenCreatedTablesArrayNotEmpty(). + * + * @return array + */ + public function dropTableWhenCreatedTablesArrayNotEmptyDataProvider() + { + return [ + ['tmp_select_table_1', false], + ['tmp_select_table', true], + ]; + } +} From 7b7b9ff54ea59fefdd542b4e08bf97434e8843d7 Mon Sep 17 00:00:00 2001 From: SolsWebdesign Date: Tue, 16 May 2017 21:03:33 +0200 Subject: [PATCH 232/363] magento/magento2#7279 Bill-to Name and Ship-to Name trancated to 20 characters in backend Compared the lengths of firstname, middlename and lastname of table "quote_address" with those of "quote" (255, 40 and 255 resp.) and of "sales_order_address" (255, 255 and 255) and with sales_order (128, 128 and 128) and choose to go with 255, 40 and 255 since these are used in "quote" as well and these are the values I know from Magento 1.9 as well. The other values are a bit inconsistent. Tested. --- .../Magento/Quote/Setup/UpgradeSchema.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/code/Magento/Quote/Setup/UpgradeSchema.php b/app/code/Magento/Quote/Setup/UpgradeSchema.php index deaef9c8a18c6..74333524497e1 100644 --- a/app/code/Magento/Quote/Setup/UpgradeSchema.php +++ b/app/code/Magento/Quote/Setup/UpgradeSchema.php @@ -46,6 +46,43 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con ] ); } + + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $setup->getConnection(self::$connectionName)->changeColumn( + $setup->getTable('quote_address', self::$connectionName), + 'firstname', + 'firstname', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 255, + 'comment' => 'Firstname' + ] + ); + } + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $setup->getConnection(self::$connectionName)->changeColumn( + $setup->getTable('quote_address', self::$connectionName), + 'middlename', + 'middlename', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 40, + 'comment' => 'Middlename' + ] + ); + } + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $setup->getConnection(self::$connectionName)->changeColumn( + $setup->getTable('quote_address', self::$connectionName), + 'lastname', + 'lastname', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 255, + 'comment' => 'Lastname' + ] + ); + } //drop foreign key for single DB case if (version_compare($context->getVersion(), '2.0.3', '<') && $setup->tableExists($setup->getTable('quote_item')) From 03da56ee61cd6d0e5cb529b2a1b6c5902eec0ce3 Mon Sep 17 00:00:00 2001 From: SolsWebdesign Date: Mon, 22 May 2017 08:17:19 +0200 Subject: [PATCH 233/363] magento/magento2#7279 Bill-to Name and Ship-to Name trancated to 20 characters in backend Compared the lengths of firstname, middlename and lastname of table "quote_address" with those of "quote" (255, 40 and 255 resp.) and of "sales_order_address" (255, 255 and 255) and with sales_order (128, 128 and 128) and choose to go with 255, 40 and 255 since these are used in "quote" as well and these are the values I know from Magento 1.9 as well. The other values are a bit inconsistent. Tested. Updated setup version of the Quote module to 2.0.5, moved all updates into a single if statement. --- .../Magento/Quote/Setup/UpgradeSchema.php | 25 ++++++++----------- app/code/Magento/Quote/etc/module.xml | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Quote/Setup/UpgradeSchema.php b/app/code/Magento/Quote/Setup/UpgradeSchema.php index 74333524497e1..b218ece780794 100644 --- a/app/code/Magento/Quote/Setup/UpgradeSchema.php +++ b/app/code/Magento/Quote/Setup/UpgradeSchema.php @@ -46,8 +46,16 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con ] ); } - - if (version_compare($context->getVersion(), '2.0.2', '<')) { + //drop foreign key for single DB case + if (version_compare($context->getVersion(), '2.0.3', '<') + && $setup->tableExists($setup->getTable('quote_item')) + ) { + $setup->getConnection()->dropForeignKey( + $setup->getTable('quote_item'), + $setup->getFkName('quote_item', 'product_id', 'catalog_product_entity', 'entity_id') + ); + } + if (version_compare($context->getVersion(), '2.0.5', '<')) { $setup->getConnection(self::$connectionName)->changeColumn( $setup->getTable('quote_address', self::$connectionName), 'firstname', @@ -58,8 +66,6 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con 'comment' => 'Firstname' ] ); - } - if (version_compare($context->getVersion(), '2.0.2', '<')) { $setup->getConnection(self::$connectionName)->changeColumn( $setup->getTable('quote_address', self::$connectionName), 'middlename', @@ -70,8 +76,6 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con 'comment' => 'Middlename' ] ); - } - if (version_compare($context->getVersion(), '2.0.2', '<')) { $setup->getConnection(self::$connectionName)->changeColumn( $setup->getTable('quote_address', self::$connectionName), 'lastname', @@ -83,15 +87,6 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con ] ); } - //drop foreign key for single DB case - if (version_compare($context->getVersion(), '2.0.3', '<') - && $setup->tableExists($setup->getTable('quote_item')) - ) { - $setup->getConnection()->dropForeignKey( - $setup->getTable('quote_item'), - $setup->getFkName('quote_item', 'product_id', 'catalog_product_entity', 'entity_id') - ); - } $setup->endSetup(); } } diff --git a/app/code/Magento/Quote/etc/module.xml b/app/code/Magento/Quote/etc/module.xml index f706b31daf21a..d0c52f9a713b6 100644 --- a/app/code/Magento/Quote/etc/module.xml +++ b/app/code/Magento/Quote/etc/module.xml @@ -6,6 +6,6 @@ */ --> - + From b84e648e3e663ff9185f02fa0217ebf355e3c7a3 Mon Sep 17 00:00:00 2001 From: Joe Constant Date: Wed, 21 Jun 2017 09:17:48 -0600 Subject: [PATCH 234/363] Change version numbers to be lower This allows for other changes that are in develop to be potentially backported at a later date --- app/code/Magento/Quote/Setup/UpgradeSchema.php | 2 +- app/code/Magento/Quote/etc/module.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Quote/Setup/UpgradeSchema.php b/app/code/Magento/Quote/Setup/UpgradeSchema.php index b218ece780794..b4a730142b7a7 100644 --- a/app/code/Magento/Quote/Setup/UpgradeSchema.php +++ b/app/code/Magento/Quote/Setup/UpgradeSchema.php @@ -55,7 +55,7 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $setup->getFkName('quote_item', 'product_id', 'catalog_product_entity', 'entity_id') ); } - if (version_compare($context->getVersion(), '2.0.5', '<')) { + if (version_compare($context->getVersion(), '2.0.4', '<')) { $setup->getConnection(self::$connectionName)->changeColumn( $setup->getTable('quote_address', self::$connectionName), 'firstname', diff --git a/app/code/Magento/Quote/etc/module.xml b/app/code/Magento/Quote/etc/module.xml index d0c52f9a713b6..337117181e444 100644 --- a/app/code/Magento/Quote/etc/module.xml +++ b/app/code/Magento/Quote/etc/module.xml @@ -6,6 +6,6 @@ */ --> - + From 80bf90aa4398dc70b56a445cc8bb072fcc057bc6 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 22 Jun 2017 09:36:04 +0300 Subject: [PATCH 235/363] MAGETWO-58876: [BP][Cloud] Mass actions are slow and consume excessive memory when merchandizing for 2.1.x --- .../Security/Model/Plugin/AuthSessionTest.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php index a8ef658b92f01..fce648b8316c8 100644 --- a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php @@ -40,6 +40,13 @@ class AuthSessionTest extends \PHPUnit_Framework_TestCase */ protected $dateTime; + /** + * Session Manager. + * + * @var \Magento\Framework\Session\SessionManager + */ + private $sessionManager; + /** * Set up */ @@ -48,14 +55,15 @@ protected function setUp() parent::setUp(); $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->objectManager->get('Magento\Framework\Config\ScopeInterface') + $this->sessionManager = $this->objectManager->create(\Magento\Framework\Session\SessionManager::class); + $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); - $this->auth = $this->objectManager->create('Magento\Backend\Model\Auth'); - $this->authSession = $this->objectManager->create('Magento\Backend\Model\Auth\Session'); - $this->adminSessionInfo = $this->objectManager->create('Magento\Security\Model\AdminSessionInfo'); + $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); + $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->adminSessionInfo = $this->objectManager->create(\Magento\Security\Model\AdminSessionInfo::class); $this->auth->setAuthStorage($this->authSession); - $this->adminSessionsManager = $this->objectManager->create('Magento\Security\Model\AdminSessionsManager'); - $this->dateTime = $this->objectManager->create('Magento\Framework\Stdlib\DateTime'); + $this->adminSessionsManager = $this->objectManager->create(\Magento\Security\Model\AdminSessionsManager::class); + $this->dateTime = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime::class); } /** From db5e297a42d5ee7c669aba315b4046cb861f36bf Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 22 Jun 2017 14:11:04 +0300 Subject: [PATCH 236/363] MAGETWO-70084: [IT] Magento\Security\Model\Plugin\AuthSessionTest::testProcessProlong failed --- .../Security/Model/Plugin/AuthSessionTest.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php index a8ef658b92f01..fce648b8316c8 100644 --- a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php @@ -40,6 +40,13 @@ class AuthSessionTest extends \PHPUnit_Framework_TestCase */ protected $dateTime; + /** + * Session Manager. + * + * @var \Magento\Framework\Session\SessionManager + */ + private $sessionManager; + /** * Set up */ @@ -48,14 +55,15 @@ protected function setUp() parent::setUp(); $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->objectManager->get('Magento\Framework\Config\ScopeInterface') + $this->sessionManager = $this->objectManager->create(\Magento\Framework\Session\SessionManager::class); + $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); - $this->auth = $this->objectManager->create('Magento\Backend\Model\Auth'); - $this->authSession = $this->objectManager->create('Magento\Backend\Model\Auth\Session'); - $this->adminSessionInfo = $this->objectManager->create('Magento\Security\Model\AdminSessionInfo'); + $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); + $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->adminSessionInfo = $this->objectManager->create(\Magento\Security\Model\AdminSessionInfo::class); $this->auth->setAuthStorage($this->authSession); - $this->adminSessionsManager = $this->objectManager->create('Magento\Security\Model\AdminSessionsManager'); - $this->dateTime = $this->objectManager->create('Magento\Framework\Stdlib\DateTime'); + $this->adminSessionsManager = $this->objectManager->create(\Magento\Security\Model\AdminSessionsManager::class); + $this->dateTime = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime::class); } /** From 3795814aa74ae72b2f6508acb9b5b3a2e67ae0e0 Mon Sep 17 00:00:00 2001 From: Sergey Shvets Date: Fri, 7 Apr 2017 13:53:14 +0300 Subject: [PATCH 237/363] MAGETWO-66276: Automated test OnePageCheckoutTest failed on variation OnePageCheckoutDhlTestVariation1 (cherry picked from commit 1e2435a) --- .../Dhl/Test/TestCase/OnePageCheckoutTest.xml | 2 +- .../Shipping/Test/Repository/ConfigData.xml | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/Dhl/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Dhl/Test/TestCase/OnePageCheckoutTest.xml index b11b6d22427c3..d557b7faa8167 100644 --- a/dev/tests/functional/tests/app/Magento/Dhl/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Dhl/Test/TestCase/OnePageCheckoutTest.xml @@ -17,7 +17,7 @@ Express easy Express easy checkmo - checkmo, dhl_eu, shipping_origin_CH, config_base_currency_ch + checkmo, dhl_eu, shipping_origin_GB, config_base_currency_gb test_type:3rd_party_test diff --git a/dev/tests/functional/tests/app/Magento/Shipping/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Shipping/Test/Repository/ConfigData.xml index a44793238759f..a9e48fb9aee50 100644 --- a/dev/tests/functional/tests/app/Magento/Shipping/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Shipping/Test/Repository/ConfigData.xml @@ -111,5 +111,31 @@ Weinbergstrasse 4
    + + + carriers + 1 + United Kingdom + GB + + + shipping + 1 + + SW1W 8JA + + + shipping + 1 + + London + + + shipping + 1 + + Bourne St + + From 85911bbf4e968741d66025204936d0a8f75c6253 Mon Sep 17 00:00:00 2001 From: Stas Kozar Date: Fri, 23 Jun 2017 12:04:15 +0300 Subject: [PATCH 238/363] MAGETWO-58533: [Backport] - Loading of order page with a lot of items in admin. - for 2.1 --- app/code/Magento/Checkout/Block/Cart.php | 10 + app/code/Magento/Checkout/Block/Cart/Grid.php | 169 ++++++ .../Magento/Checkout/Block/Cart/Sidebar.php | 43 +- .../Cart/CheckoutSummaryConfigProvider.php | 65 +++ .../Test/Unit/Block/Cart/GridTest.php | 221 ++++++++ .../Test/Unit/Block/Cart/SidebarTest.php | 44 +- .../CheckoutSummaryConfigProviderTest.php | 58 +++ .../Magento/Checkout/etc/adminhtml/system.xml | 11 +- app/code/Magento/Checkout/etc/config.xml | 3 + app/code/Magento/Checkout/etc/frontend/di.xml | 1 + .../frontend/layout/checkout_cart_index.xml | 2 +- .../view/frontend/templates/cart/form.phtml | 8 +- .../view/frontend/web/js/model/cart/cache.js | 191 +++++++ .../web/js/model/cart/estimate-service.js | 98 ++-- .../js/model/cart/totals-processor/default.js | 148 ++++-- .../view/frontend/web/js/view/minicart.js | 29 +- .../web/js/view/summary/cart-items.js | 102 +++- .../web/template/minicart/content.html | 19 +- .../web/template/summary/cart-items.html | 55 +- .../GiftMessage/Model/OrderItemRepository.php | 25 +- .../Unit/Model/OrderItemRepositoryTest.php | 490 ++++++++++++++++++ app/code/Magento/Sales/Block/Order/Items.php | 94 +++- .../Sales/CustomerData/LastOrderedItems.php | 19 +- app/code/Magento/Sales/Model/Order.php | 32 +- .../CustomerData/LastOrderedItemsTest.php | 173 +++++++ .../Sales/Test/Unit/Model/OrderTest.php | 238 +++++++-- app/code/Magento/Sales/etc/config.xml | 1 + .../view/frontend/layout/sales_order_view.xml | 1 + .../view/frontend/templates/order/items.phtml | 85 +-- .../web/css/source/module/_cart.less | 64 +++ .../module/checkout/_order-summary.less | 18 + .../Magento_Sales/web/css/source/_module.less | 67 +++ .../web/css/source/module/_cart.less | 57 ++ .../module/checkout/_order-summary.less | 17 + .../Magento_Sales/web/css/source/_module.less | 59 ++- .../Test/TestStep/CreateProductsStep.php | 32 +- .../MergePreconditionProductsStep.php | 51 ++ .../Checkout/Test/Block/Cart/Pager.php | 49 ++ .../Checkout/Test/Block/Cart/Shipping.php | 11 +- .../Checkout/Test/Block/Cart/Sidebar.php | 95 +++- .../Test/Block/Onepage/AbstractReview.php | 68 ++- .../AssertItemsCounterInMiniShoppingCart.php | 43 ++ ...rtLinkGoToCartNotPresentInSummaryBlock.php | 40 ++ ...ssertLinkGoToCartPresentInSummaryBlock.php | 40 ++ .../AssertPagersNotPresentInShoppingCart.php | 43 ++ .../AssertPagersPresentInShoppingCart.php | 42 ++ .../Constraint/AssertPagersSummaryText.php | 54 ++ .../AssertSubtotalInMiniShoppingCart.php | 46 ++ ...tVisibleItemsQtyInCheckoutSummaryBlock.php | 61 +++ ...ssertVisibleItemsQtyInMiniShoppingCart.php | 60 +++ ...sibleItemsQtyMessageInMiniShoppingCart.php | 64 +++ ...eItemsQtyMessageOnCheckoutSummaryBlock.php | 75 +++ .../Checkout/Test/Page/CheckoutCart.xml | 2 + .../Checkout/Test/Repository/ConfigData.xml | 72 +++ .../AddProductsToShoppingCartEntityTest.php | 20 +- .../AddProductsToShoppingCartEntityTest.xml | 100 ++++ .../DeleteProductFromMiniShoppingCartTest.xml | 18 + .../Test/TestCase/ShoppingCartPagerTest.php | 59 +++ .../Test/TestCase/ShoppingCartPagerTest.xml | 34 ++ .../TestStep/AddProductsToTheCartStep.php | 28 +- .../RemoveProductsFromTheCartStep.php | 84 +++ .../Magento/Checkout/Test/etc/testcase.xml | 7 + .../Magento/Sales/Test/Block/Order/View.php | 34 ++ ...sertOrderItemsPagerDisplayedOnFrontend.php | 71 +++ .../AssertOrderItemsPagerHiddenOnFrontend.php | 71 +++ .../Sales/Test/Repository/OrderInjectable.xml | 48 ++ .../Test/TestCase/FrontendOrderPagerTest.php | 40 ++ .../Test/TestCase/FrontendOrderPagerTest.xml | 19 + .../app/Magento/Sales/Test/etc/testcase.xml | 3 + .../Magento/Backend/Block/MenuTest.php | 29 +- .../Magento/Checkout/Block/Cart/GridTest.php | 31 ++ .../Magento/Checkout/Block/CartTest.php | 36 +- .../Magento/Sales/Block/Order/ItemsTest.php | 135 +++++ .../Magento/Sales/_files/order_item_list.php | 56 ++ .../Security/Model/Plugin/AuthSessionTest.php | 18 +- .../Integrity/_files/blacklist/reference.txt | 1 + 76 files changed, 4225 insertions(+), 382 deletions(-) create mode 100644 app/code/Magento/Checkout/Block/Cart/Grid.php create mode 100644 app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php create mode 100644 app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php create mode 100644 app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/model/cart/cache.js create mode 100644 app/code/Magento/GiftMessage/Test/Unit/Model/OrderItemRepositoryTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Catalog/Test/TestStep/MergePreconditionProductsStep.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Pager.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertItemsCounterInMiniShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertLinkGoToCartNotPresentInSummaryBlock.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertLinkGoToCartPresentInSummaryBlock.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPagersNotPresentInShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPagersPresentInShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPagersSummaryText.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInMiniShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertVisibleItemsQtyInCheckoutSummaryBlock.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertVisibleItemsQtyInMiniShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertVisibleItemsQtyMessageInMiniShoppingCart.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertVisibleItemsQtyMessageOnCheckoutSummaryBlock.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/Repository/ConfigData.xml create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.xml create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/RemoveProductsFromTheCartStep.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderItemsPagerDisplayedOnFrontend.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderItemsPagerHiddenOnFrontend.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/FrontendOrderPagerTest.php create mode 100644 dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/FrontendOrderPagerTest.xml create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/GridTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Block/Order/ItemsTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_item_list.php diff --git a/app/code/Magento/Checkout/Block/Cart.php b/app/code/Magento/Checkout/Block/Cart.php index 0b80561d4573e..04159a5a7566a 100644 --- a/app/code/Magento/Checkout/Block/Cart.php +++ b/app/code/Magento/Checkout/Block/Cart.php @@ -232,4 +232,14 @@ public function getItemsCount() { return $this->getQuote()->getItemsCount(); } + + /** + * Render pagination HTML. + * + * @return string + */ + public function getPagerHtml() + { + return $this->getChildHtml('pager'); + } } diff --git a/app/code/Magento/Checkout/Block/Cart/Grid.php b/app/code/Magento/Checkout/Block/Cart/Grid.php new file mode 100644 index 0000000000000..699508c6e22d2 --- /dev/null +++ b/app/code/Magento/Checkout/Block/Cart/Grid.php @@ -0,0 +1,169 @@ + than number from + * Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager and + * custom_items weren't set to cart block + */ +class Grid extends \Magento\Checkout\Block\Cart +{ + /** + * Config settings path to determine when pager on checkout/cart/index will be visible. + */ + const XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER = 'checkout/cart/number_items_to_display_pager'; + + /** + * @var \Magento\Quote\Model\ResourceModel\Quote\Item\Collection + */ + private $itemsCollection; + + /** + * @var \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory + * + */ + private $itemCollectionFactory; + + /** + * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface + */ + private $joinAttributeProcessor; + + /** + * Is display pager on shopping cart page. + * + * @var bool + */ + private $isPagerDisplayed; + + /** + * Grid constructor. + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrlBuilder + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Framework\App\Http\Context $httpContext + * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory + * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor + * @param array $data + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + \Magento\Catalog\Model\ResourceModel\Url $catalogUrlBuilder, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Framework\App\Http\Context $httpContext, + \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory, + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, + array $data = [] + ) { + $this->itemCollectionFactory = $itemCollectionFactory; + $this->joinAttributeProcessor = $joinProcessor; + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $catalogUrlBuilder, + $cartHelper, + $httpContext, + $data + ); + } + + /** + * Prepare Quote Item Product URLs. + * When we don't have custom_items, items URLs will be collected for Collection limited by pager + * Pager limit on checkout/cart/index is determined by configuration + * Configuration path is Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager + * + * @return void + */ + protected function _construct() + { + if (!$this->isPagerDisplayedOnPage()) { + parent::_construct(); + } + if ($this->hasData('template')) { + $this->setTemplate($this->getData('template')); + } + } + + /** + * {@inheritdoc} + */ + protected function _prepareLayout() + { + parent::_prepareLayout(); + if ($this->isPagerDisplayedOnPage()) { + $availableLimit = (int)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $itemsCollection = $this->getItemsForGrid(); + /** @var \Magento\Theme\Block\Html\Pager $pager */ + $pager = $this->getLayout()->createBlock(\Magento\Theme\Block\Html\Pager::class); + $pager->setAvailableLimit([$availableLimit => $availableLimit])->setCollection($itemsCollection); + $this->setChild('pager', $pager); + $itemsCollection->load(); + $this->prepareItemUrls(); + } + return $this; + } + + /** + * Prepare quote items collection for pager. + * + * @return \Magento\Quote\Model\ResourceModel\Quote\Item\Collection + */ + public function getItemsForGrid() + { + if (!$this->itemsCollection) { + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item\Collection $itemCollection */ + $itemCollection = $this->itemCollectionFactory->create(); + + $itemCollection->setQuote($this->getQuote()); + $itemCollection->addFieldToFilter('parent_item_id', ['null' => true]); + $this->joinAttributeProcessor->process($itemCollection); + + $this->itemsCollection = $itemCollection; + } + return $this->itemsCollection; + } + + /** + * {@inheritdoc} + */ + public function getItems() + { + if (!$this->isPagerDisplayedOnPage()) { + return parent::getItems(); + } + return $this->getItemsForGrid()->getItems(); + } + + /** + * Verify if display pager on shopping cart. + * If cart block has custom_items and items qty in the shopping cartisPagerDisplayed) { + $availableLimit = (int)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $this->isPagerDisplayed = !$this->getCustomItems() && $availableLimit < $this->getItemsCount(); + } + return $this->isPagerDisplayed; + } +} diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 03c705d8a4751..4d51c613c08e8 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -8,22 +8,22 @@ use Magento\Store\Model\ScopeInterface; /** - * Cart sidebar block + * Cart sidebar block. */ class Sidebar extends AbstractCart { /** - * Xml pah to checkout sidebar display value + * Xml pah to checkout sidebar display value. */ const XML_PATH_CHECKOUT_SIDEBAR_DISPLAY = 'checkout/sidebar/display'; /** - * Xml pah to checkout sidebar count value + * Xml pah to checkout sidebar count value. */ const XML_PATH_CHECKOUT_SIDEBAR_COUNT = 'checkout/sidebar/count'; /** - * @var \Magento\Catalog\Helper\Image + * @var \Magento\Catalog\Helper\Image. */ protected $imageHelper; @@ -56,7 +56,7 @@ public function __construct( } /** - * Returns minicart config + * Returns minicart config. * * @return array */ @@ -70,7 +70,8 @@ public function getConfig() 'imageTemplate' => $this->getImageHtmlTemplate(), 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), - 'websiteId' => $this->_storeManager->getStore()->getWebsiteId() + 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), ]; } @@ -85,7 +86,7 @@ public function getImageHtmlTemplate() } /** - * Get one page checkout page url + * Get one page checkout page url. * * @codeCoverageIgnore * @return string @@ -96,7 +97,7 @@ public function getCheckoutUrl() } /** - * Get shopping cart page url + * Get shopping cart page url. * * @return string * @codeCoverageIgnore @@ -107,7 +108,7 @@ public function getShoppingCartUrl() } /** - * Get update cart item url + * Get update cart item url. * * @return string * @codeCoverageIgnore @@ -118,7 +119,7 @@ public function getUpdateItemQtyUrl() } /** - * Get remove cart item url + * Get remove cart item url. * * @return string * @codeCoverageIgnore @@ -129,7 +130,7 @@ public function getRemoveItemUrl() } /** - * Define if Mini Shopping Cart Pop-Up Menu enabled + * Define if Mini Shopping Cart Pop-Up Menu enabled. * * @return bool * @codeCoverageIgnore @@ -144,7 +145,7 @@ public function getIsNeedToDisplaySideBar() } /** - * Return totals from custom quote if needed + * Return totals from custom quote if needed. * * @return array */ @@ -158,7 +159,7 @@ public function getTotalsCache() } /** - * Retrieve subtotal block html + * Retrieve subtotal block html. * * @codeCoverageIgnore * @return string @@ -180,7 +181,7 @@ public function getBaseUrl() } /** - * Return max visible item count for minicart + * Return max visible item count for minicart. * * @return int */ @@ -188,4 +189,18 @@ private function getMiniCartMaxItemsCount() { return (int)$this->_scopeConfig->getValue('checkout/sidebar/count', ScopeInterface::SCOPE_STORE); } + + /** + * Returns maximum cart items to display. + * This setting regulates how many items will be displayed in minicart. + * + * @return int + */ + private function getMaxItemsToDisplay() + { + return (int)$this->_scopeConfig->getValue( + 'checkout/sidebar/max_items_display_count', + ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php b/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php new file mode 100644 index 0000000000000..545758e64813a --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php @@ -0,0 +1,65 @@ +urlBuilder = $urlBuilder; + $this->scopeConfig = $scopeConfig; + } + + /** + * {@inheritdoc} + */ + public function getConfig() + { + return [ + 'maxCartItemsToDisplay' => $this->getMaxCartItemsToDisplay(), + 'cartUrl' => $this->urlBuilder->getUrl('checkout/cart') + ]; + } + + /** + * Returns maximum cart items to display. + * This setting regulates how many items will be displayed in checkout summary block + * + * @return int + */ + private function getMaxCartItemsToDisplay() + { + return (int)$this->scopeConfig->getValue( + 'checkout/options/max_items_display_count', + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php new file mode 100644 index 0000000000000..9f21be2178624 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php @@ -0,0 +1,221 @@ +itemCollectionFactoryMock = + $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->joinAttributeProcessorMock = + $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class) + ->getMockForAbstractClass(); + $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->checkoutSessionMock = $this->getMockBuilder(\Magento\Checkout\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->itemCollectionMock = $objectManagerHelper + ->getCollectionMock(\Magento\Quote\Model\ResourceModel\Quote\Item\Collection::class, []); + $this->quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->layoutMock = $this->getMockBuilder(\Magento\Framework\View\LayoutInterface::class) + ->getMockForAbstractClass(); + $this->pagerBlockMock = $this->getMockBuilder(\Magento\Theme\Block\Html\Pager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkoutSessionMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + $this->quoteMock->expects($this->any())->method('getAllVisibleItems')->willReturn([]); + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with( + \Magento\Checkout\Block\Cart\Grid::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null + )->willReturn(20); + $this->block = $objectManagerHelper->getObject( + \Magento\Checkout\Block\Cart\Grid::class, + [ + 'itemCollectionFactory' => $this->itemCollectionFactoryMock, + 'joinAttributeProcessor' => $this->joinAttributeProcessorMock, + 'scopeConfig' => $this->scopeConfigMock, + 'checkoutSession' => $this->checkoutSessionMock, + 'layout' => $this->layoutMock, + 'data' => ['template' => 'cart/form1.phtml'], + ] + ); + } + + public function testGetTemplate() + { + $this->assertEquals('cart/form1.phtml', $this->block->getTemplate()); + } + + public function testGetItemsForGrid() + { + $this->getMockItemsForGrid(); + + $this->assertEquals($this->itemCollectionMock, $this->block->getItemsForGrid()); + } + + /** + * @cover \Magento\Checkout\Block\Cart\Grid::_prepareLayout + */ + public function testSetLayout() + { + $itemsCount = 150; + $availableLimit = 20; + $this->getMockItemsForGrid(); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn($itemsCount); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with( + \Magento\Checkout\Block\Cart\Grid::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null + )->willReturn($availableLimit); + $this->layoutMock + ->expects($this->once()) + ->method('createBlock') + ->with(\Magento\Theme\Block\Html\Pager::class) + ->willReturn($this->pagerBlockMock); + $this->pagerBlockMock + ->expects($this->once()) + ->method('setAvailableLimit') + ->with([$availableLimit => $availableLimit]) + ->willReturnSelf(); + $this->pagerBlockMock + ->expects($this->once()) + ->method('setCollection') + ->with($this->itemCollectionMock) + ->willReturnSelf(); + $this->layoutMock->expects($this->once())->method('setChild')->with(null, null, 'pager'); + $this->itemCollectionMock->expects($this->once())->method('load')->willReturnSelf(); + $this->quoteMock->expects($this->never())->method('getAllVisibleItems'); + $this->itemCollectionMock->expects($this->once())->method('getItems')->willReturn([]); + $this->block->setLayout($this->layoutMock); + } + + public function testGetItems() + { + $this->getMockItemsForGrid(); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(20); + $this->itemCollectionMock->expects($this->once())->method('getItems')->willReturn(['expected']); + + $this->assertEquals(['expected'], $this->block->getItems()); + } + + private function getMockItemsForGrid() + { + $this->itemCollectionFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($this->itemCollectionMock); + $this->checkoutSessionMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + $this->itemCollectionMock->expects($this->once())->method('setQuote')->with($this->quoteMock)->willReturnSelf(); + $this->itemCollectionMock + ->expects($this->once()) + ->method('addFieldToFilter') + ->with('parent_item_id', ['null' => true]) + ->willReturnSelf(); + $this->joinAttributeProcessorMock->expects($this->once())->method('process')->with($this->itemCollectionMock); + } + + /** + * @cover \Magento\Checkout\Block\Cart::prepareItemUrls + */ + public function testGetItemsIfCustomItemsExists() + { + $itemMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->getMockForAbstractClass(); + $storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + $objectManagerHelper = new ObjectManagerHelper($this); + + $this->block = $objectManagerHelper->getObject( + \Magento\Checkout\Block\Cart\Grid::class, + [ + 'itemCollectionFactory' => $this->itemCollectionFactoryMock, + 'joinAttributeProcessor' => $this->joinAttributeProcessorMock, + 'scopeConfig' => $this->scopeConfigMock, + 'checkoutSession' => $this->checkoutSessionMock, + 'layout' => $this->layoutMock, + 'data' => ['custom_items' => [$itemMock]], + 'storeManager' => $storeManager, + ] + ); + + $this->assertEquals([$itemMock], $this->block->getItems()); + } + + public function testGetItemsWhenPagerNotVisible() + { + $this->assertEquals([], $this->block->getItems()); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index d8b156ae159c4..fb5564cddcea1 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -5,6 +5,11 @@ */ namespace Magento\Checkout\Test\Unit\Block\Cart; +/** + * Test for Magento\Checkout\Block\Cart\SideBar. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SidebarTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -54,14 +59,14 @@ protected function setUp() { $this->_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->requestMock = $this->getMock('\Magento\Framework\App\RequestInterface'); - $this->layoutMock = $this->getMock('\Magento\Framework\View\Layout', [], [], '', false); - $this->checkoutSessionMock = $this->getMock('\Magento\Checkout\Model\Session', [], [], '', false); - $this->urlBuilderMock = $this->getMock('\Magento\Framework\UrlInterface', [], [], '', false); - $this->storeManagerMock = $this->getMock('\Magento\Store\Model\StoreManagerInterface', [], [], '', false); - $this->imageHelper = $this->getMock('Magento\Catalog\Helper\Image', [], [], '', false); + $this->requestMock = $this->getMock(\Magento\Framework\App\RequestInterface::clas); + $this->layoutMock = $this->getMock(\Magento\Framework\View\Layout::class, [], [], '', false); + $this->checkoutSessionMock = $this->getMock(\Magento\Checkout\Model\Session::class, [], [], '', false); + $this->urlBuilderMock = $this->getMock(\Magento\Framework\UrlInterface::class, [], [], '', false); + $this->storeManagerMock = $this->getMock(\Magento\Store\Model\StoreManagerInterface::clas, [], [], '', false); + $this->imageHelper = $this->getMock(\Magento\Catalog\Helper\Image::class, [], [], '', false); $this->scopeConfigMock = $this->getMock( - '\Magento\Framework\App\Config\ScopeConfigInterface', + \Magento\Framework\App\Config\ScopeConfigInterface::class, [], [], '', @@ -69,7 +74,7 @@ protected function setUp() ); $contextMock = $this->getMock( - '\Magento\Framework\View\Element\Template\Context', + \Magento\Framework\View\Element\Template\Context::class, ['getLayout', 'getUrlBuilder', 'getStoreManager', 'getScopeConfig', 'getRequest'], [], '', @@ -92,7 +97,7 @@ protected function setUp() ->will($this->returnValue($this->requestMock)); $this->model = $this->_objectManager->getObject( - 'Magento\Checkout\Block\Cart\Sidebar', + \Magento\Checkout\Block\Cart\Sidebar::class, [ 'context' => $contextMock, 'imageHelper' => $this->imageHelper, @@ -104,7 +109,7 @@ protected function setUp() public function testGetTotalsHtml() { $totalsHtml = "$134.36"; - $totalsBlockMock = $this->getMockBuilder('\Magento\Checkout\Block\Shipping\Price') + $totalsBlockMock = $this->getMockBuilder(\Magento\Checkout\Block\Shipping\Price::class) ->disableOriginalConstructor() ->setMethods(['toHtml']) ->getMock(); @@ -123,7 +128,7 @@ public function testGetTotalsHtml() public function testGetConfig() { - $storeMock = $this->getMock('\Magento\Store\Model\Store', [], [], '', false); + $storeMock = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $websiteId = 100; $shoppingCartUrl = 'http://url.com/cart'; @@ -141,7 +146,8 @@ public function testGetConfig() 'imageTemplate' => $imageTemplate, 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, - 'websiteId' => $websiteId + 'websiteId' => $websiteId, + 'maxItemsToDisplay' => 8, ]; $valueMap = [ @@ -160,15 +166,21 @@ public function testGetConfig() ->willReturnMap($valueMap); $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); $this->imageHelper->expects($this->once())->method('getFrame')->willReturn(false); - - $this->scopeConfigMock->expects($this->once()) + $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') ->with( \Magento\Checkout\Block\Cart\Sidebar::XML_PATH_CHECKOUT_SIDEBAR_COUNT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE )->willReturn(3); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with( + 'checkout/sidebar/max_items_display_count', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + )->willReturn(8); + + $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); $this->assertEquals($expectedResult, $this->model->getConfig()); } @@ -187,7 +199,7 @@ public function testGetIsNeedToDisplaySideBar() public function testGetTotalsCache() { - $quoteMock = $this->getMock('\Magento\Quote\Model\Quote', [], [], '', false); + $quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); $totalsMock = ['totals']; $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getTotals')->willReturn($totalsMock); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php new file mode 100644 index 0000000000000..d05361e1a31eb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php @@ -0,0 +1,58 @@ +urlBuilderMock = $this->getMockBuilder(UrlInterface::class)->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)->getMock(); + $this->model = new CheckoutSummaryConfigProvider($this->urlBuilderMock, $this->scopeConfigMock); + } + + public function testGetConfig() + { + $maxItemsCount = 10; + $cartUrl = 'url/to/cart/page'; + $expectedResult = [ + 'maxCartItemsToDisplay' => $maxItemsCount, + 'cartUrl' => $cartUrl + ]; + + $this->urlBuilderMock->expects($this->once())->method('getUrl')->with('checkout/cart')->willReturn($cartUrl); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('checkout/options/max_items_display_count', ScopeInterface::SCOPE_STORE) + ->willReturn($maxItemsCount); + + $this->assertEquals($expectedResult, $this->model->getConfig()); + } +} diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 0b0ec9e276f89..fec2ec4fd6f4c 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -25,6 +25,9 @@ \Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions + + + @@ -35,6 +38,9 @@ Magento\Config\Model\Config\Source\Yesno + + + @@ -50,7 +56,10 @@ Magento\Config\Model\Config\Source\Yesno - + + + + diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index 4bac74fb0efdc..dcc63a978e0a3 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -11,10 +11,12 @@ 1 1 + 10 30 0 + 20 1 @@ -22,6 +24,7 @@ 1 5 + 10 general diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 2ef7a2498fd6a..40385740a6130 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -47,6 +47,7 @@ Magento\Checkout\Model\DefaultConfigProvider + Magento\Checkout\Model\Cart\CheckoutSummaryConfigProvider diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 0c619e89cc9f6..6e5a8fb0f1017 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -180,7 +180,7 @@ - + diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 44a842f311f8c..9d170d4d73ae2 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -6,7 +6,7 @@ // @codingStandardsIgnoreFile -/** @var $block \Magento\Checkout\Block\Cart */ +/** @var $block \Magento\Checkout\Block\Cart\Grid */ ?> helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> getChildHtml('form_before') ?> @@ -17,6 +17,9 @@ class="form form-cart"> getBlockHtml('formkey'); ?>
    + getPagerHtml()): ?> +
    getPagerHtml(); ?>
    + load rates from cache + if (!cartCache.isChanged('address', quote.shippingAddress()) && + !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + cartCache.get('rates') + ) { + shippingService.setShippingRates(cartCache.get('rates')); + + return; } - }); - quote.shippingMethod.subscribe(function () { - totalsDefaultProvider.estimateTotals(quote.shippingAddress()); - }); - quote.billingAddress.subscribe(function () { - var type = quote.billingAddress().getType(); - if (quote.isVirtual()) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] - ? totalsProcessors[type].estimateTotals(quote.billingAddress()) - : totalsProcessors['default'].estimateTotals(quote.billingAddress()); - } - }); - } -); + + // update rates list when estimated address was set + rateProcessors['default'] = defaultProcessor; + rateProcessors[type] ? + rateProcessors[type].getRates(quote.shippingAddress()) : + rateProcessors['default'].getRates(quote.shippingAddress()); + + // save rates to cache after load + shippingService.getShippingRates().subscribe(function (rates) { + cartCache.set('rates', rates); + }); + } + }); + quote.shippingMethod.subscribe(function () { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + }); + quote.billingAddress.subscribe(function () { + var type = quote.billingAddress().getType(); + + if (quote.isVirtual()) { + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.billingAddress()) : + totalsProcessors['default'].estimateTotals(quote.billingAddress()); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js index 2ed0f1fdc4773..1aff03ea32b49 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js @@ -2,55 +2,103 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'underscore', - 'Magento_Checkout/js/model/resource-url-manager', - 'Magento_Checkout/js/model/quote', - 'mage/storage', - 'Magento_Checkout/js/model/totals', - 'Magento_Checkout/js/model/error-processor' - ], - function (_, resourceUrlManager, quote, storage, totalsService, errorProcessor) { - 'use strict'; - - return { - requiredFields: ['countryId', 'region', 'regionId', 'postcode'], - - /** - * Get shipping rates for specified address. - */ - estimateTotals: function (address) { - var serviceUrl, payload; - totalsService.isLoading(true); - serviceUrl = resourceUrlManager.getUrlForTotalsEstimationForNewAddress(quote), - payload = { - addressInformation: { - address: _.pick(address, this.requiredFields) - } - }; - - if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { - payload.addressInformation['shipping_method_code'] = quote.shippingMethod()['method_code']; - payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code']; - } - - storage.post( - serviceUrl, JSON.stringify(payload), false - ).done( - function (result) { - quote.setTotals(result); - } - ).fail( - function (response) { - errorProcessor.process(response); - } - ).always( - function () { - totalsService.isLoading(false); - } - ); + +define([ + 'underscore', + 'Magento_Checkout/js/model/resource-url-manager', + 'Magento_Checkout/js/model/quote', + 'mage/storage', + 'Magento_Checkout/js/model/totals', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Checkout/js/model/cart/cache', + 'Magento_Customer/js/customer-data', +], function (_, resourceUrlManager, quote, storage, totalsService, errorProcessor, cartCache, customerData) { + 'use strict'; + + /** + * Load data from server. + * + * @param {Object} address + */ + var loadFromServer = function (address) { + var serviceUrl, + payload; + + // Start loader for totals block + totalsService.isLoading(true); + serviceUrl = resourceUrlManager.getUrlForTotalsEstimationForNewAddress(quote); + payload = { + addressInformation: { + address: _.pick(address, cartCache.requiredFields) } }; - } -); + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + payload.addressInformation['shipping_method_code'] = quote.shippingMethod()['method_code']; + payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code']; + } + + storage.post( + serviceUrl, JSON.stringify(payload), false + ).done(function (result) { + var data = { + totals: result, + address: address, + cartVersion: customerData.get('cart')()['data_id'], + shippingMethodCode: null, + shippingCarrierCode: null + }; + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + data.shippingMethodCode = quote.shippingMethod()['method_code']; + data.shippingCarrierCode = quote.shippingMethod()['carrier_code']; + } + + quote.setTotals(result); + cartCache.set('cart-data', data); + }).fail(function (response) { + errorProcessor.process(response); + }).always(function () { + // Stop loader for totals block + totalsService.isLoading(false); + }); + }; + + return { + /** + * Array of required address fields. + * + * @property {Array.String} requiredFields + * @deprecated Use cart cache. + */ + requiredFields: cartCache.requiredFields, + + /** + * Get shipping rates for specified address. + * + * @param {Object} address + */ + estimateTotals: function (address) { + var data = { + shippingMethodCode: null, + shippingCarrierCode: null + }; + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + data.shippingMethodCode = quote.shippingMethod()['method_code']; + data.shippingCarrierCode = quote.shippingMethod()['carrier_code']; + } + + if (!cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + !cartCache.isChanged('shippingMethodCode', data.shippingMethodCode) && + !cartCache.isChanged('shippingCarrierCode', data.shippingCarrierCode) && + !cartCache.isChanged('address', address) && + cartCache.get('totals') + ) { + quote.setTotals(cartCache.get('totals')); + } else { + loadFromServer(address); + } + } + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index c885b51aa138e..8d54413581631 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -2,13 +2,15 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + define([ 'uiComponent', 'Magento_Customer/js/customer-data', 'jquery', 'ko', 'underscore', - 'sidebar' + 'sidebar', + 'mage/translate' ], function (Component, customerData, $, ko, _) { 'use strict'; @@ -76,6 +78,7 @@ define([ return Component.extend({ shoppingCartUrl: window.checkout.shoppingCartUrl, + maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, /** @@ -144,6 +147,7 @@ define([ /** * Get cart param by name. + * * @param {String} name * @returns {*} */ @@ -155,6 +159,29 @@ define([ } return this.cart[name](); + }, + + /** + * Returns array of cart items, limited by 'maxItemsToDisplay' setting. + * + * @returns [] + */ + getCartItems: function () { + var items = this.getCartParam('items') || []; + items = items.slice(parseInt(-this.maxItemsToDisplay, 10)); + + return items; + }, + + /** + * Returns count of cart line items. + * + * @returns {Number} + */ + getCartLineItemsCount: function () { + var items = this.getCartParam('items') || []; + + return parseInt(items.length, 10); } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js index 1c6a2ae4d1b2d..6bc822db563b5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js @@ -2,30 +2,80 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/*browser:true*/ -/*global define*/ -define( - [ - 'ko', - 'Magento_Checkout/js/model/totals', - 'uiComponent', - 'Magento_Checkout/js/model/step-navigator', - 'Magento_Checkout/js/model/quote' - ], - function (ko, totals, Component, stepNavigator, quote) { - 'use strict'; - return Component.extend({ - defaults: { - template: 'Magento_Checkout/summary/cart-items' - }, - totals: totals.totals(), - getItems: totals.getItems(), - getItemsQty: function() { - return parseFloat(this.totals.items_qty); - }, - isItemsBlockExpanded: function () { - return quote.isVirtual() || stepNavigator.isProcessed('shipping'); + +define([ + 'ko', + 'Magento_Checkout/js/model/totals', + 'uiComponent', + 'Magento_Checkout/js/model/step-navigator', + 'Magento_Checkout/js/model/quote' +], function (ko, totals, Component, stepNavigator, quote) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/summary/cart-items' + }, + totals: totals.totals(), + items: ko.observable([]), + maxCartItemsToDisplay: window.checkoutConfig.maxCartItemsToDisplay, + cartUrl: window.checkoutConfig.cartUrl, + + /** + * @deprecated Please use observable property (this.items()) + */ + getItems: totals.getItems(), + + /** + * Returns cart items qty. + * + * @returns {Number} + */ + getItemsQty: function () { + return parseFloat(this.totals['items_qty']); + }, + + /** + * Returns count of cart line items. + * + * @returns {Number} + */ + getCartLineItemsCount: function () { + return parseInt(totals.getItems()().length, 10); + }, + + /** + * @inheritdoc + */ + initialize: function () { + this._super(); + // Set initial items to observable field + this.setItems(totals.getItems()()); + // Subscribe for items data changes and refresh items in view + totals.getItems().subscribe(function (items) { + this.setItems(items); + }.bind(this)); + }, + + /** + * Set items to observable field. + * + * @param {Object} items + */ + setItems: function (items) { + if (items && items.length > 0) { + items = items.slice(parseInt(-this.maxCartItemsToDisplay, 10)); } - }); - } -); + this.items(items); + }, + + /** + * Returns bool value for items block state (expanded or not). + * + * @returns {*|Boolean} + */ + isItemsBlockExpanded: function () { + return quote.isVirtual() || stepNavigator.isProcessed('shipping'); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 8cbc1fdbb29dc..487ea923a8a44 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -27,12 +27,19 @@
    - - - + + - - + + + + + + + + + +
    @@ -65,7 +72,7 @@
    -
      +
        diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 3ec8571a81647..60d88653924c0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -4,22 +4,26 @@ * See COPYING.txt for license details. */ --> - -
        +
        - - - + + + + - - + + + + + +
          - +
        1. @@ -31,34 +35,13 @@
        -
        - - -
        -
        - - - - - - - - -
        -
        -
        -
          - -
        1. -
          - - - -
          -
        2. - -
        + +
        +
        +
        - diff --git a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php index bc22a1a64bad8..164ce8d7ff13b 100644 --- a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php +++ b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php @@ -23,6 +23,13 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor */ protected $orderFactory; + /** + * Cached orders data. + * + * @var \Magento\Sales\Api\Data\OrderInterface[] + */ + private $orders; + /** * Store manager interface. * @@ -127,6 +134,7 @@ public function save($orderId, $orderItemId, \Magento\GiftMessage\Api\Data\Messa $this->giftMessageSaveModel->setGiftmessages($message); try { $this->giftMessageSaveModel->saveAllInOrder(); + unset($this->orders[$orderId]); } catch (\Exception $e) { throw new CouldNotSaveException(__('Could not add gift message to order: "%1"', $e->getMessage()), $e); } @@ -142,14 +150,19 @@ public function save($orderId, $orderItemId, \Magento\GiftMessage\Api\Data\Messa */ protected function getItemById($orderId, $orderItemId) { - /** @var \Magento\Sales\Api\Data\OrderInterface $order */ - $order = $this->orderFactory->create()->load($orderId); + if (!isset($this->orders[$orderId])) { + $this->orders[$orderId] = $this->orderFactory->create()->load($orderId); + } + + /** @var \Magento\Sales\Api\Data\OrderInterface $item */ + $order = $this->orders[$orderId]; /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ - foreach ($order->getItems() as $item) { - if ($item->getItemId() === $orderItemId) { - return $item; - } + $item = $order->getItemById($orderItemId); + + if ($item !== null) { + return $item; } + return false; } } diff --git a/app/code/Magento/GiftMessage/Test/Unit/Model/OrderItemRepositoryTest.php b/app/code/Magento/GiftMessage/Test/Unit/Model/OrderItemRepositoryTest.php new file mode 100644 index 0000000000000..efe03f7ca14a7 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Unit/Model/OrderItemRepositoryTest.php @@ -0,0 +1,490 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getItemById', 'getIsVirtual']) + ->getMock(); + $this->orderFactoryMock = $this->getMockBuilder(\Magento\Sales\Model\OrderFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->helperMock = $this->getMockBuilder(\Magento\GiftMessage\Helper\Message::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->messageFactoryMock = $this->getMockBuilder(\Magento\GiftMessage\Model\MessageFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->giftMessageSaveModelMock = $this->getMockBuilder(\Magento\GiftMessage\Model\Save::class) + ->disableOriginalConstructor() + ->setMethods(['setGiftmessages', 'saveAllInOrder']) + ->getMock(); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->orderItemRepository = $helper->getObject( + \Magento\GiftMessage\Model\OrderItemRepository::class, + [ + 'orderFactory' => $this->orderFactoryMock, + 'storeManager' => $this->storeManagerMock, + 'helper' => $this->helperMock, + 'messageFactory' => $this->messageFactoryMock, + 'giftMessageSaveModel' => $this->giftMessageSaveModelMock + ] + ); + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::get + */ + public function testGet() + { + $orderId = 1; + $orderItemId = 2; + $messageId = 3; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Model\Message::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(true); + $orderItemMock->expects($this->once()) + ->method('getGiftMessageId') + ->willReturn($messageId); + $this->messageFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($messageMock); + $messageMock->expects($this->once()) + ->method('load') + ->with($messageId) + ->willReturnSelf(); + + $this->assertEquals($messageMock, $this->orderItemRepository->get($orderId, $orderItemId)); + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::get + */ + public function testGetNoSuchEntityExceptionOnGetItemById() + { + $orderId = 1; + $orderItemId = 2; + + $this->orderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn(null); + $this->helperMock->expects($this->never())->method('isMessagesAllowed'); + + try { + $this->orderItemRepository->get($orderId, $orderItemId); + $this->fail('Expected NoSuchEntityException not caught'); + } catch (NoSuchEntityException $exception) { + $this->assertEquals('There is no item with provided id in the order', $exception->getMessage()); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::get + */ + public function testGetNoSuchEntityExceptionOnIsMessageAllowed() + { + $orderId = 1; + $orderItemId = 2; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + + $this->orderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(false); + $orderItemMock->expects($this->never())->method('getGiftMessageId'); + + try { + $this->orderItemRepository->get($orderId, $orderItemId); + $this->fail('Expected NoSuchEntityException not caught'); + } catch (NoSuchEntityException $exception) { + $this->assertEquals( + 'There is no item with provided id in the order or gift message isn\'t allowed', + $exception->getMessage() + ); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::get + */ + public function testGetNoSuchEntityExceptionOnGetGiftMessageId() + { + $orderId = 1; + $orderItemId = 2; + $messageId = null; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + + $this->orderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(true); + $orderItemMock->expects($this->once()) + ->method('getGiftMessageId') + ->willReturn($messageId); + $this->messageFactoryMock->expects($this->never())->method('create'); + + try { + $this->orderItemRepository->get($orderId, $orderItemId); + $this->fail('Expected NoSuchEntityException not caught'); + } catch (NoSuchEntityException $exception) { + $this->assertEquals('There is no item with provided id in the order', $exception->getMessage()); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::save + */ + public function testSave() + { + $orderId = 1; + $orderItemId = 2; + $message[$orderItemId] = [ + 'type' => 'order_item', + 'sender' => 'sender_value', + 'recipient' => 'recipient_value', + 'message' => 'message_value', + ]; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Api\Data\MessageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->orderMock->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(true); + $messageMock->expects($this->once()) + ->method('getSender') + ->willReturn('sender_value'); + $messageMock->expects($this->once()) + ->method('getRecipient') + ->willReturn('recipient_value'); + $messageMock->expects($this->once()) + ->method('getMessage') + ->willReturn('message_value'); + $this->giftMessageSaveModelMock->expects($this->once()) + ->method('setGiftmessages') + ->with($message); + $this->giftMessageSaveModelMock->expects($this->once()) + ->method('saveAllInOrder'); + + $this->assertTrue($this->orderItemRepository->save($orderId, $orderItemId, $messageMock)); + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::save + */ + public function testSaveNoSuchEntityException() + { + $orderId = 1; + $orderItemId = 2; + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Api\Data\MessageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn(null); + $this->orderMock->expects($this->never()) + ->method('getIsVirtual'); + + try { + $this->orderItemRepository->save($orderId, $orderItemId, $messageMock); + $this->fail('Expected NoSuchEntityException not caught'); + } catch (NoSuchEntityException $exception) { + $this->assertEquals('There is no item with provided id in the order', $exception->getMessage()); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::save + */ + public function testSaveInvalidTransitionException() + { + $orderId = 1; + $orderItemId = 2; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Api\Data\MessageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->orderMock->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(true); + $this->helperMock->expects($this->never()) + ->method('isMessagesAllowed'); + + try { + $this->orderItemRepository->save($orderId, $orderItemId, $messageMock); + $this->fail('Expected InvalidTransitionException not caught'); + } catch (InvalidTransitionException $exception) { + $this->assertEquals('Gift Messages are not applicable for virtual products', $exception->getMessage()); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::save + */ + public function testSaveCouldNotSaveException() + { + $orderId = 1; + $orderItemId = 2; + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Api\Data\MessageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->orderMock->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(false); + $messageMock->expects($this->never()) + ->method('getSender'); + + try { + $this->orderItemRepository->save($orderId, $orderItemId, $messageMock); + $this->fail('Expected CouldNotSaveException not caught'); + } catch (CouldNotSaveException $exception) { + $this->assertEquals('Gift Message is not available', $exception->getMessage()); + } + } + + /** + * @covers \Magento\GiftMessage\Model\OrderItemRepository::save + */ + public function testSaveCouldNotSaveExceptionOnSaveAllInOrder() + { + $orderId = 1; + $orderItemId = 2; + $message[$orderItemId] = [ + 'type' => 'order_item', + 'sender' => 'sender_value', + 'recipient' => 'recipient_value', + 'message' => 'message_value', + ]; + $excep = new \Exception('Exception message'); + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getGiftMessageId']) + ->getMock(); + $messageMock = $this->getMockBuilder(\Magento\GiftMessage\Api\Data\MessageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getItemById') + ->with($orderItemId) + ->willReturn($orderItemMock); + $this->orderMock->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + $this->helperMock->expects($this->once()) + ->method('isMessagesAllowed') + ->with('order_item', $orderItemMock, $this->storeMock) + ->willReturn(true); + $messageMock->expects($this->once()) + ->method('getSender') + ->willReturn('sender_value'); + $messageMock->expects($this->once()) + ->method('getRecipient') + ->willReturn('recipient_value'); + $messageMock->expects($this->once()) + ->method('getMessage') + ->willReturn('message_value'); + $this->giftMessageSaveModelMock->expects($this->once()) + ->method('setGiftmessages') + ->with($message); + $this->giftMessageSaveModelMock->expects($this->once()) + ->method('saveAllInOrder') + ->will($this->throwException($excep)); + + try { + $this->orderItemRepository->save($orderId, $orderItemId, $messageMock); + $this->fail('Expected CouldNotSaveException not caught'); + } catch (CouldNotSaveException $exception) { + $this->assertEquals( + 'Could not add gift message to order: "' . $excep->getMessage() . '"', + $exception->getMessage() + ); + } + } +} diff --git a/app/code/Magento/Sales/Block/Order/Items.php b/app/code/Magento/Sales/Block/Order/Items.php index 2b908d01b36d5..af5082d4b7ef5 100644 --- a/app/code/Magento/Sales/Block/Order/Items.php +++ b/app/code/Magento/Sales/Block/Order/Items.php @@ -14,28 +14,116 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems { /** - * Core registry + * Core registry. * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; + /** + * Order items per page. + * + * @var int + */ + private $itemsPerPage; + + /** + * Flat sales order payment collection factory. + * + * @var \Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory + */ + private $itemCollectionFactory; + + /** + * Flat sales order payment collection. + * + * @var \Magento\Sales\Model\ResourceModel\Order\Item\Collection|null + */ + private $itemCollection; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param \Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory|null $itemCollectionFactory */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + \Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory $itemCollectionFactory = null ) { $this->_coreRegistry = $registry; + $this->itemCollectionFactory = $itemCollectionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory::class); parent::__construct($context, $data); } /** - * Retrieve current order model instance + * Init pager block and item collection with page size and current page number. + * + * @return $this + */ + protected function _prepareLayout() + { + $this->itemsPerPage = $this->_scopeConfig->getValue('sales/orders/items_per_page'); + + $this->itemCollection = $this->itemCollectionFactory->create(); + $this->itemCollection->setOrderFilter($this->getOrder()); + $this->itemCollection->filterByParent(null); + + /** @var \Magento\Theme\Block\Html\Pager $pagerBlock */ + $pagerBlock = $this->getChildBlock('sales_order_item_pager'); + if ($pagerBlock) { + $pagerBlock->setLimit($this->itemsPerPage); + //here pager updates collection parameters + $pagerBlock->setCollection($this->itemCollection); + $pagerBlock->setAvailableLimit([$this->itemsPerPage]); + $pagerBlock->setShowAmounts($this->isPagerDisplayed()); + } + + return parent::_prepareLayout(); + } + + /** + * Determine if the pager should be displayed for order items list. + * To be called from templates(after _prepareLayout()). + * + * @return bool + */ + public function isPagerDisplayed() + { + $pagerBlock = $this->getChildBlock('sales_order_item_pager'); + + return $pagerBlock && ($this->itemCollection->getSize() > $this->itemsPerPage); + } + + /** + * Get visible items for current page. + * To be called from templates(after _prepareLayout()). + * + * @return \Magento\Framework\DataObject[] + */ + public function getItems() + { + return $this->itemCollection->getItems(); + } + + /** + * Get pager HTML according to our requirements. + * To be called from templates(after _prepareLayout()). + * + * @return string HTML output + */ + public function getPagerHtml() + { + /** @var \Magento\Theme\Block\Html\Pager $pagerBlock */ + $pagerBlock = $this->getChildBlock('sales_order_item_pager'); + return $pagerBlock ? $pagerBlock->toHtml() : ''; + } + + /** + * Retrieve current order model instance. * * @return \Magento\Sales\Model\Order */ diff --git a/app/code/Magento/Sales/CustomerData/LastOrderedItems.php b/app/code/Magento/Sales/CustomerData/LastOrderedItems.php index df510552010fe..ac354535baada 100644 --- a/app/code/Magento/Sales/CustomerData/LastOrderedItems.php +++ b/app/code/Magento/Sales/CustomerData/LastOrderedItems.php @@ -7,10 +7,15 @@ use Magento\Customer\CustomerData\SectionSourceInterface; +/** + * Returns information for "Recently Ordered" widget. + * It contains limited list of salable products from the last placed order. + * Qty of products to display is limited by LastOrderedItems::SIDEBAR_ORDER_LIMIT constant. + */ class LastOrderedItems implements SectionSourceInterface { /** - * Limit of orders in side bar + * Limit of orders in side bar. */ const SIDEBAR_ORDER_LIMIT = 5; @@ -44,6 +49,13 @@ class LastOrderedItems implements SectionSourceInterface */ protected $stockRegistry; + /** + * Store manager interface. + * + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $_storeManager; + /** * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory * @param \Magento\Sales\Model\Order\Config $orderConfig @@ -66,7 +78,7 @@ public function __construct( } /** - * Init customer order for display on front + * Init last placed customer order to display on front. * * @return void */ @@ -96,8 +108,9 @@ protected function getItems() if ($order) { $website = $this->_storeManager->getStore()->getWebsiteId(); + /** @var \Magento\Sales\Model\Order\Item $item */ foreach ($order->getParentItemsRandomCollection($limit) as $item) { - if ($item->getProduct() && in_array($website, $item->getProduct()->getWebsiteIds())) { + if ($item->hasData('product') && in_array($website, $item->getProduct()->getWebsiteIds())) { $items[] = [ 'id' => $item->getId(), 'name' => $item->getName(), diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 1b6c53482909e..09e03dca659cb 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -184,6 +184,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface /** * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @deprecated Remove unused dependency. */ protected $productRepository; @@ -778,38 +779,24 @@ protected function _canReorder($ignoreSalable = false) } $products = []; - foreach ($this->getItemsCollection() as $item) { + $itemsCollection = $this->getItemsCollection(); + foreach ($itemsCollection as $item) { $products[] = $item->getProductId(); } if (!empty($products)) { - /* - * @TODO ACPAOC: Use product collection here, but ensure that product - * is loaded with order store id, otherwise there'll be problems with isSalable() - * for composite products - * - */ - /* - $productsCollection = $this->_productFactory->create()->getCollection() + $productsCollection = $this->productListFactory->create() ->setStoreId($this->getStoreId()) ->addIdFilter($products) ->addAttributeToSelect('status') ->load(); - foreach ($productsCollection as $product) { - if (!$product->isSalable()) { + foreach ($itemsCollection as $item) { + $product = $productsCollection->getItemById($item->getProductId()); + if (!$product) { return false; } - } - */ - - foreach ($products as $productId) { - try { - $product = $this->productRepository->getById($productId, false, $this->getStoreId()); - if (!$ignoreSalable && !$product->isSalable()) { - return false; - } - } catch (NoSuchEntityException $noEntityException) { + if (!$ignoreSalable && !$product->isSalable()) { return false; } } @@ -1451,6 +1438,7 @@ public function setPayment(\Magento\Sales\Api\Data\OrderPaymentInterface $paymen } /*********************** STATUSES ***************************/ + /** * Return collection of order status history items. * @@ -2004,6 +1992,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderExtensionInt } //@codeCoverageIgnoreStart + /** * Returns adjustment_negative * @@ -4352,5 +4341,6 @@ public function setShippingMethod($shippingMethod) { return $this->setData('shipping_method', $shippingMethod); } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php b/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php new file mode 100644 index 0000000000000..482dd6ad3e81d --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php @@ -0,0 +1,173 @@ +objectManagerHelper = new ObjectManagerHelper($this); + + $this->orderCollectionFactoryMock = + $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->orderConfigMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerSessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockRegistryMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) + ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->section = new \Magento\Sales\CustomerData\LastOrderedItems( + $this->orderCollectionFactoryMock, + $this->orderConfigMock, + $this->customerSessionMock, + $this->stockRegistryMock, + $this->storeManagerMock + ); + } + + public function testGetSectionData() + { + $websiteId = 4; + $expectedItem = [ + 'id' => 1, + 'name' => 'Product Name', + 'url' => 'http://example.com', + 'is_saleable' => true, + ]; + $productId = 10; + + $stockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) + ->getMockForAbstractClass(); + $itemWithProductMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $itemWithoutProductMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $items = [$itemWithoutProductMock, $itemWithProductMock]; + $this->getLastOrderMock(); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $storeMock->expects($this->any())->method('getWebsiteId')->willReturn($websiteId); + $this->orderMock->expects($this->once()) + ->method('getParentItemsRandomCollection') + ->with(\Magento\Sales\CustomerData\LastOrderedItems::SIDEBAR_ORDER_LIMIT) + ->willReturn($items); + + $itemWithProductMock->expects($this->once())->method('hasData')->with('product')->willReturn(true); + $itemWithProductMock->expects($this->any())->method('getProduct')->willReturn($productMock); + $productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1, 4]); + $itemWithProductMock->expects($this->once())->method('getId')->willReturn($expectedItem['id']); + $itemWithProductMock->expects($this->once())->method('getName')->willReturn($expectedItem['name']); + $productMock->expects($this->once())->method('getProductUrl')->willReturn($expectedItem['url']); + $this->stockRegistryMock->expects($this->once())->method('getStockItem')->willReturn($stockItemMock); + $productMock->expects($this->once())->method('getId')->willReturn($productId); + $itemWithProductMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $this->stockRegistryMock + ->expects($this->once()) + ->method('getStockItem') + ->with($productId, $websiteId) + ->willReturn($stockItemMock); + $stockItemMock->expects($this->once())->method('getIsInStock')->willReturn($expectedItem['is_saleable']); + $itemWithoutProductMock->expects($this->once())->method('hasData')->with('product')->willReturn(false); + + $this->assertEquals(['items' => [$expectedItem]], $this->section->getSectionData()); + } + + private function getLastOrderMock() + { + $customerId = 1; + $visibleOnFrontStatuses = ['complete']; + + $orderCollectionMock = $this->objectManagerHelper + ->getCollectionMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class, [$this->orderMock]); + $this->customerSessionMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->orderConfigMock + ->expects($this->once()) + ->method('getVisibleOnFrontStatuses') + ->willReturn($visibleOnFrontStatuses); + $this->orderCollectionFactoryMock->expects($this->once())->method('create')->willReturn($orderCollectionMock); + $orderCollectionMock->expects($this->at(0)) + ->method('addAttributeToFilter') + ->with('customer_id', $customerId) + ->willReturnSelf(); + $orderCollectionMock->expects($this->at(1)) + ->method('addAttributeToFilter') + ->with('status', ['in' => $visibleOnFrontStatuses]) + ->willReturnSelf(); + $orderCollectionMock->expects($this->once()) + ->method('addAttributeToSort') + ->willReturnSelf(); + $orderCollectionMock->expects($this->once()) + ->method('setPage') + ->willReturnSelf(); + + return $this->orderMock; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index a1d8d3c4ae59f..0c8c5e994ba2f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -6,14 +6,15 @@ namespace Magento\Sales\Test\Unit\Model; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Order\Status\History\CollectionFactory as HistoryCollectionFactory; -use Magento\Sales\Model\ResourceModel\Order\Item; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; /** * Test class for \Magento\Sales\Model\Order + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class OrderTest extends \PHPUnit_Framework_TestCase { @@ -68,9 +69,9 @@ class OrderTest extends \PHPUnit_Framework_TestCase protected $salesOrderCollectionMock; /** - * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ProductCollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ - private $productRepository; + private $productCollectionFactoryMock; protected function setUp() { @@ -96,48 +97,50 @@ protected function setUp() '', false ); - $this->salesOrderCollectionFactoryMock = $this->getMock( - 'Magento\Sales\Model\ResourceModel\Order\CollectionFactory', + $this->productCollectionFactoryMock = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create'], [], '', false ); - $this->item = $this->getMock( - 'Magento\Sales\Model\ResourceModel\Order\Item', - ['isDeleted', 'getQtyToInvoice', 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice'], + $this->salesOrderCollectionFactoryMock = $this->getMock( + \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class, + ['create'], [], '', false ); - $this->item = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->setMethods([ + $this->item = $this->getMock( + \Magento\Sales\Model\ResourceModel\Order\Item::class, + [ 'isDeleted', 'getQtyToInvoice', 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice', 'getProductId' - ]) - ->getMock(); - $this->salesOrderCollectionMock = $this->getMockBuilder('Magento\Sales\Model\ResourceModel\Order\Collection') - ->disableOriginalConstructor() + ], + [], + '', + false + ); + $this->salesOrderCollectionMock = $this->getMockBuilder( + \Magento\Sales\Model\ResourceModel\Order\Collection::class + )->disableOriginalConstructor() ->setMethods(['addFieldToFilter', 'load', 'getFirstItem']) ->getMock(); - $collection = $this->getMock('Magento\Sales\Model\ResourceModel\Order\Item\Collection', [], [], '', false); - $collection->expects($this->any()) - ->method('setOrderFilter') - ->willReturnSelf(); - $collection->expects($this->any()) - ->method('getItems') - ->willReturn([$this->item]); - $collection->expects(self::any()) - ->method('getIterator') - ->willReturn(new \ArrayIterator([$this->item])); - $this->orderItemCollectionFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($collection); + $collection = $this->getMock( + \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, + [], + [], + '', + false + ); + $collection->expects($this->any())->method('setOrderFilter')->willReturnSelf(); + $collection->expects($this->any())->method('getItems')->willReturn([$this->item]); + $collection->expects($this->any())->method('getIterator')->willReturn(new \ArrayIterator([$this->item])); + $this->orderItemCollectionFactoryMock->expects($this->any())->method('create')->willReturn($collection); $this->priceCurrency = $this->getMockForAbstractClass( 'Magento\Framework\Pricing\PriceCurrencyInterface', @@ -149,15 +152,10 @@ protected function setUp() ['round'] ); - $this->productRepository = $this->getMockBuilder(ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->incrementId = '#00000001'; - $this->eventManager = $this->getMock('Magento\Framework\Event\Manager', [], [], '', false); - $context = $this->getMock('Magento\Framework\Model\Context', ['getEventDispatcher'], [], '', false); - $context->expects($this->any()) - ->method('getEventDispatcher') - ->willReturn($this->eventManager); + $this->eventManager = $this->getMock(\Magento\Framework\Event\Manager::class, [], [], '', false); + $context = $this->getMock(\Magento\Framework\Model\Context::class, ['getEventDispatcher'], [], '', false); + $context->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManager); $this->order = $helper->getObject( 'Magento\Sales\Model\Order', @@ -169,7 +167,7 @@ protected function setUp() 'historyCollectionFactory' => $this->historyCollectionFactoryMock, 'salesOrderCollectionFactory' => $this->salesOrderCollectionFactoryMock, 'priceCurrency' => $this->priceCurrency, - 'productRepository' => $this->productRepository + 'productListFactory' => $this->productCollectionFactoryMock ] ); } @@ -405,7 +403,7 @@ public function testCanReorder() $this->order->setState(Order::STATE_PROCESSING); $this->order->setActionFlag(Order::ACTION_FLAG_REORDER, true); - $this->item->expects(static::once()) + $this->item->expects($this->any()) ->method('getProductId') ->willReturn($productId); @@ -416,10 +414,29 @@ public function testCanReorder() ->method('isSalable') ->willReturn(true); - $this->productRepository->expects(static::once()) - ->method('getById') - ->with($productId, false) + $productCollection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'addIdFilter', 'load', 'getItemById', 'addAttributeToSelect']) + ->getMock(); + $productCollection->expects($this->once()) + ->method('setStoreId') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('getItemById') + ->with($productId) ->willReturn($product); + $this->productCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($productCollection); $this->assertTrue($this->order->canReorder()); } @@ -455,7 +472,7 @@ public function testCanReorderProductNotExists() $this->order->setState(Order::STATE_PROCESSING); $this->order->setActionFlag(Order::ACTION_FLAG_REORDER, true); - $this->item->expects(static::once()) + $this->item->expects($this->any()) ->method('getProductId') ->willReturn($productId); @@ -465,10 +482,29 @@ public function testCanReorderProductNotExists() $product->expects(static::never()) ->method('isSalable'); - $this->productRepository->expects(static::once()) - ->method('getById') - ->with($productId, false) - ->willThrowException(new NoSuchEntityException(__('Requested product doesn\'t exist'))); + $productCollection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'addIdFilter', 'load', 'getItemById', 'addAttributeToSelect']) + ->getMock(); + $productCollection->expects($this->once()) + ->method('setStoreId') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('getItemById') + ->with($productId) + ->willReturn(null); + $this->productCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($productCollection); + $productCollection->expects($this->once()) + ->method('addAttributeToSelect') + ->willReturnSelf(); $this->assertFalse($this->order->canReorder()); } @@ -483,7 +519,7 @@ public function testCanReorderProductNotSalable() $this->order->setState(Order::STATE_PROCESSING); $this->order->setActionFlag(Order::ACTION_FLAG_REORDER, true); - $this->item->expects(static::once()) + $this->item->expects($this->any()) ->method('getProductId') ->willReturn($productId); @@ -494,10 +530,29 @@ public function testCanReorderProductNotSalable() ->method('isSalable') ->willReturn(false); - $this->productRepository->expects(static::once()) - ->method('getById') - ->with($productId, false) + $productCollection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'addIdFilter', 'load', 'getItemById', 'addAttributeToSelect']) + ->getMock(); + $productCollection->expects($this->once()) + ->method('setStoreId') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $productCollection->expects($this->once()) + ->method('getItemById') + ->with($productId) ->willReturn($product); + $this->productCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($productCollection); + $productCollection->expects($this->once()) + ->method('addAttributeToSelect') + ->willReturnSelf(); $this->assertFalse($this->order->canReorder()); } @@ -891,6 +946,85 @@ public function testLoadByIncrementIdAndStoreId() $this->assertSame($this->order, $this->order->loadByIncrementIdAndStoreId($incrementId, $storeId)); } + /** + * Test method setPayment with Id. + * + * @return void + */ + public function testSetPaymentWithId() + { + $this->order->setId(123); + $payment = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) + ->disableOriginalConstructor() + ->getMock(); + $this->order->setData(OrderInterface::PAYMENT, $payment); + $this->order->setDataChanges(false); + + $payment->expects($this->once()) + ->method('setOrder') + ->with($this->order) + ->willReturnSelf(); + + $payment->expects($this->once()) + ->method('setParentId') + ->with(123) + ->willReturnSelf(); + + $payment->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $this->order->setPayment($payment); + + $this->assertEquals( + $this->order->getData( + OrderInterface::PAYMENT + ), + $payment + ); + + $this->assertFalse( + $this->order->hasDataChanges() + ); + } + + /** + * Test method setPayment without Id. + * + * @return void + */ + public function testSetPaymentNoId() + { + $this->order->setId(123); + $this->order->setDataChanges(false); + $payment = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) + ->disableOriginalConstructor() + ->getMock(); + $payment->expects($this->once()) + ->method('setOrder') + ->with($this->order) + ->willReturnSelf(); + $payment->expects($this->once()) + ->method('setParentId') + ->with(123) + ->willReturnSelf(); + $payment->expects($this->any()) + ->method('getId') + ->willReturn(null); + $this->order->setPayment($payment); + + $this->assertEquals( + $this->order->getData( + OrderInterface::PAYMENT + ), + $payment + ); + + $this->assertTrue( + $this->order->hasDataChanges() + ); + } + public function notInvoicingStatesProvider() { return [ diff --git a/app/code/Magento/Sales/etc/config.xml b/app/code/Magento/Sales/etc/config.xml index 3961f19f2cf60..6bd5e5800c689 100644 --- a/app/code/Magento/Sales/etc/config.xml +++ b/app/code/Magento/Sales/etc/config.xml @@ -22,6 +22,7 @@ 1 + 20 480 diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_order_view.xml b/app/code/Magento/Sales/view/frontend/layout/sales_order_view.xml index 4bf37ca59189e..c680cf48b91bc 100644 --- a/app/code/Magento/Sales/view/frontend/layout/sales_order_view.xml +++ b/app/code/Magento/Sales/view/frontend/layout/sales_order_view.xml @@ -26,6 +26,7 @@ + colspan="4" class="mark" diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml index a100910e82aaf..72e2a899e18f0 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml @@ -6,13 +6,19 @@ // @codingStandardsIgnoreFile +/** @var \Magento\Sales\Block\Order\Items $block */ ?> -getOrder() ?> - -
        -
    - - +
    +
    + + + isPagerDisplayed()): ?> + + + + @@ -20,55 +26,58 @@ - - getItemsCollection(); ?> - - count(); ?> - - - getParentItem()) { - continue; -} ?> - - getItemHtml($_item) ?> - helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $_item) && $_item->getGiftMessageId()): ?> - helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($_item); ?> + + getItems(); ?> + + + getParentItem()) continue; ?> + + getItemHtml($item) ?> + helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $item) && $item->getGiftMessageId()): ?> + helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($item); ?> - - - + + + + isPagerDisplayed()): ?> + + + + getChildHtml('order_totals') ?> - -
    + getPagerHtml() ?> +
    + id="order-item-gift-message-link-getId() ?>" + class="action show" + aria-controls="order-item-gift-message-getId() ?>" + data-item-id="getId() ?>"> - helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($_item); ?> -
    + getPagerHtml();?> +
    -
    - -