Skip to content

Commit e9f6fa6

Browse files
committed
Merge remote-tracking branch 'origin/2.1-develop' into 2.1-develop-pr33
2 parents 1878a64 + a57a612 commit e9f6fa6

File tree

24 files changed

+689
-54
lines changed

24 files changed

+689
-54
lines changed

app/code/Magento/Catalog/Model/Category.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* @method Category setUrlPath(string $urlPath)
2828
* @method Category getSkipDeleteChildren()
2929
* @method Category setSkipDeleteChildren(boolean $value)
30+
* @method Category setChangedProductIds(array $categoryIds) Set products ids that inserted or deleted for category
31+
* @method array getChangedProductIds() Get products ids that inserted or deleted for category
3032
*
3133
* @SuppressWarnings(PHPMD.LongVariable)
3234
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Catalog\Model\Category\Product;
7+
8+
/**
9+
* Resolver to get product positions by ids assigned to specific category
10+
*/
11+
class PositionResolver extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
12+
{
13+
/**
14+
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
15+
* @param string $connectionName
16+
*/
17+
public function __construct(
18+
\Magento\Framework\Model\ResourceModel\Db\Context $context,
19+
$connectionName = null
20+
) {
21+
parent::__construct($context, $connectionName);
22+
}
23+
24+
/**
25+
* Initialize resource model
26+
*
27+
* @return void
28+
*/
29+
protected function _construct()
30+
{
31+
$this->_init('catalog_product_entity', 'entity_id');
32+
}
33+
34+
/**
35+
* Get category product positions
36+
*
37+
* @param int $categoryId
38+
* @return array
39+
*/
40+
public function getPositions($categoryId)
41+
{
42+
$connection = $this->getConnection();
43+
44+
$select = $connection->select()->from(
45+
['cpe' => $this->getTable('catalog_product_entity')],
46+
'entity_id'
47+
)->joinLeft(
48+
['ccp' => $this->getTable('catalog_category_product')],
49+
'ccp.product_id=cpe.entity_id'
50+
)->where(
51+
'ccp.category_id = ?',
52+
$categoryId
53+
)->order(
54+
'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC
55+
);
56+
57+
return array_flip($connection->fetchCol($select));
58+
}
59+
}

app/code/Magento/Catalog/Model/ResourceModel/Category.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,18 @@ protected function _saveCategoryProducts($category)
409409
* Update product positions in category
410410
*/
411411
if (!empty($update)) {
412+
$newPositions = [];
412413
foreach ($update as $productId => $position) {
413-
$where = ['category_id = ?' => (int)$id, 'product_id = ?' => (int)$productId];
414-
$bind = ['position' => (int)$position];
414+
$delta = $position - $oldProducts[$productId];
415+
if (!isset($newPositions[$delta])) {
416+
$newPositions[$delta] = [];
417+
}
418+
$newPositions[$delta][] = $productId;
419+
}
420+
421+
foreach ($newPositions as $delta => $productIds) {
422+
$bind = ['position' => new \Zend_Db_Expr("position + ({$delta})")];
423+
$where = ['category_id = ?' => (int)$id, 'product_id IN (?)' => $productIds];
415424
$connection->update($this->getCategoryProductTable(), $bind, $where);
416425
}
417426
}
@@ -422,6 +431,8 @@ protected function _saveCategoryProducts($category)
422431
'catalog_category_change_products',
423432
['category' => $category, 'product_ids' => $productIds]
424433
);
434+
435+
$category->setChangedProductIds($productIds);
425436
}
426437

427438
if (!empty($insert) || !empty($update) || !empty($delete)) {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Catalog\Test\Unit\Model\Category\Product;
7+
8+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
9+
use Magento\Catalog\Model\Category\Product\PositionResolver;
10+
use Magento\Framework\Model\ResourceModel\Db\Context;
11+
use Magento\Framework\App\ResourceConnection;
12+
use Magento\Framework\DB\Adapter\AdapterInterface;
13+
use Magento\Framework\DB\Select;
14+
15+
class PositionResolverTest extends \PHPUnit_Framework_TestCase
16+
{
17+
/**
18+
* @var Context|\PHPUnit_Framework_MockObject_MockObject
19+
*/
20+
private $context;
21+
22+
/**
23+
* @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject
24+
*/
25+
private $resources;
26+
27+
/**
28+
* @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject
29+
*/
30+
private $connection;
31+
32+
/**
33+
* @var Select|\PHPUnit_Framework_MockObject_MockObject
34+
*/
35+
private $select;
36+
37+
/**
38+
* @var PositionResolver
39+
*/
40+
private $model;
41+
42+
/**
43+
* @var array
44+
*/
45+
private $positions = [
46+
'3' => 100,
47+
'2' => 101,
48+
'1' => 102
49+
];
50+
51+
private $flippedPositions = [
52+
'100' => 3,
53+
'101' => 2,
54+
'102' => 1
55+
];
56+
57+
protected function setUp()
58+
{
59+
$this->context = $this->getMockBuilder(Context::class)
60+
->disableOriginalConstructor()
61+
->getMock();
62+
63+
$this->resources = $this->getMockBuilder(ResourceConnection::class)
64+
->disableOriginalConstructor()
65+
->getMock();
66+
67+
$this->connection = $this->getMockBuilder(AdapterInterface::class)
68+
->disableOriginalConstructor()
69+
->getMockForAbstractClass();
70+
71+
$this->select = $this->getMockBuilder(Select::class)
72+
->disableOriginalConstructor()
73+
->getMock();
74+
75+
$this->model = (new ObjectManager($this))->getObject(
76+
PositionResolver::class,
77+
[
78+
'context' => $this->context,
79+
null,
80+
'_resources' => $this->resources
81+
]
82+
);
83+
}
84+
85+
public function testGetPositions()
86+
{
87+
$this->resources->expects($this->once())
88+
->method('getConnection')
89+
->willReturn($this->connection);
90+
91+
$this->connection->expects($this->once())
92+
->method('select')
93+
->willReturn($this->select);
94+
$this->select->expects($this->once())
95+
->method('from')
96+
->willReturnSelf();
97+
$this->select->expects($this->once())
98+
->method('where')
99+
->willReturnSelf();
100+
$this->select->expects($this->once())
101+
->method('order')
102+
->willReturnSelf();
103+
$this->select->expects($this->once())
104+
->method('joinLeft')
105+
->willReturnSelf();
106+
$this->connection->expects($this->once())
107+
->method('fetchCol')
108+
->willReturn($this->positions);
109+
110+
$this->assertEquals($this->flippedPositions, $this->model->getPositions(1));
111+
}
112+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier;
8+
9+
use Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Alerts\Price;
10+
use Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Alerts\Stock;
11+
use Magento\Framework\App\Config\ScopeConfigInterface;
12+
use Magento\Framework\View\LayoutFactory;
13+
use Magento\Store\Model\ScopeInterface;
14+
use Magento\Ui\Component\Form\Fieldset;
15+
16+
class Alerts extends AbstractModifier
17+
{
18+
const DATA_SCOPE = 'data';
19+
const DATA_SCOPE_STOCK = 'stock';
20+
const DATA_SCOPE_PRICE = 'price';
21+
22+
/**
23+
* @var string
24+
*/
25+
private static $previousGroup = 'related';
26+
27+
/**
28+
* @var int
29+
*/
30+
private static $sortOrder = 110;
31+
32+
/**
33+
* @var ScopeConfigInterface
34+
*/
35+
private $scopeConfig;
36+
37+
/**
38+
* @var LayoutFactory
39+
*/
40+
private $layoutFactory;
41+
42+
/**
43+
* Alerts constructor.
44+
* @param ScopeConfigInterface $scopeConfig
45+
* @param LayoutFactory $layoutFactory
46+
*/
47+
public function __construct(
48+
ScopeConfigInterface $scopeConfig,
49+
LayoutFactory $layoutFactory
50+
) {
51+
$this->scopeConfig = $scopeConfig;
52+
$this->layoutFactory = $layoutFactory;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
* @since 101.0.0
58+
*/
59+
public function modifyData(array $data)
60+
{
61+
return $data;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
* @since 101.0.0
67+
*/
68+
public function modifyMeta(array $meta)
69+
{
70+
if (!$this->canShowTab()) {
71+
return $meta;
72+
}
73+
74+
$meta = array_replace_recursive(
75+
$meta,
76+
[
77+
'alerts' => [
78+
'arguments' => [
79+
'data' => [
80+
'config' => [
81+
'additionalClasses' => 'admin__fieldset-section',
82+
'label' => __('Product Alerts'),
83+
'collapsible' => true,
84+
'componentType' => Fieldset::NAME,
85+
'dataScope' => static::DATA_SCOPE,
86+
'sortOrder' =>
87+
$this->getNextGroupSortOrder(
88+
$meta,
89+
self::$previousGroup,
90+
self::$sortOrder
91+
),
92+
],
93+
],
94+
],
95+
'children' => [
96+
static::DATA_SCOPE_STOCK => $this->getAlertStockFieldset(),
97+
static::DATA_SCOPE_PRICE => $this->getAlertPriceFieldset()
98+
],
99+
],
100+
]
101+
);
102+
103+
return $meta;
104+
}
105+
106+
/**
107+
* @return bool
108+
*/
109+
private function canShowTab()
110+
{
111+
$alertPriceAllow = $this->scopeConfig->getValue(
112+
'catalog/productalert/allow_price',
113+
ScopeInterface::SCOPE_STORE
114+
);
115+
$alertStockAllow = $this->scopeConfig->getValue(
116+
'catalog/productalert/allow_stock',
117+
ScopeInterface::SCOPE_STORE
118+
);
119+
120+
return ($alertPriceAllow || $alertStockAllow);
121+
}
122+
123+
/**
124+
* Prepares config for the alert stock products fieldset
125+
* @return array
126+
*/
127+
private function getAlertStockFieldset()
128+
{
129+
return [
130+
'arguments' => [
131+
'data' => [
132+
'config' => [
133+
'label' => __('Alert stock'),
134+
'componentType' => 'container',
135+
'component' => 'Magento_Ui/js/form/components/html',
136+
'additionalClasses' => 'admin__fieldset-note',
137+
'content' =>
138+
'<h4>' . __('Alert Stock') . '</h4>' .
139+
$this->layoutFactory->create()->createBlock(
140+
Stock::class
141+
)->toHtml(),
142+
]
143+
]
144+
]
145+
];
146+
}
147+
148+
/**
149+
* Prepares config for the alert price products fieldset
150+
* @return array
151+
*/
152+
private function getAlertPriceFieldset()
153+
{
154+
return [
155+
'arguments' => [
156+
'data' => [
157+
'config' => [
158+
'label' => __('Alert price'),
159+
'componentType' => 'container',
160+
'component' => 'Magento_Ui/js/form/components/html',
161+
'additionalClasses' => 'admin__fieldset-note',
162+
'content' =>
163+
'<h4>' . __('Alert Price') . '</h4>' .
164+
$this->layoutFactory->create()->createBlock(
165+
Price::class
166+
)->toHtml(),
167+
]
168+
]
169+
]
170+
];
171+
}
172+
}

app/code/Magento/Catalog/etc/adminhtml/di.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@
142142
<item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Attributes</item>
143143
<item name="sortOrder" xsi:type="number">120</item>
144144
</item>
145+
<item name="alerts" xsi:type="array">
146+
<item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Alerts</item>
147+
<item name="sortOrder" xsi:type="number">130</item>
148+
</item>
145149
</argument>
146150
</arguments>
147151
</virtualType>

0 commit comments

Comments
 (0)