From c6a1455fd87d45032cea50c4447431c173456a65 Mon Sep 17 00:00:00 2001 From: Hlavtox Date: Mon, 17 Jul 2023 08:18:19 +0200 Subject: [PATCH] Add selections feature --- config.xml | 2 +- ps_facetedsearch.php | 104 +++++++++++------- src/Adapter/MySQL.php | 14 ++- src/Filters/Block.php | 96 ++++++++++++++++ src/Filters/Converter.php | 35 ++++++ src/Product/Search.php | 46 ++++++++ src/Product/SearchProvider.php | 6 +- tests/php/FacetedSearch/Adapter/MySQLTest.php | 12 +- upgrade/upgrade-3.14.0.php | 32 ++++++ views/templates/admin/add.tpl | 30 +++++ 10 files changed, 323 insertions(+), 54 deletions(-) create mode 100644 upgrade/upgrade-3.14.0.php diff --git a/config.xml b/config.xml index 0ab6f7f39..e2426dd61 100644 --- a/config.xml +++ b/config.xml @@ -2,7 +2,7 @@ ps_facetedsearch - + diff --git a/ps_facetedsearch.php b/ps_facetedsearch.php index eb20d0659..97d901f67 100644 --- a/ps_facetedsearch.php +++ b/ps_facetedsearch.php @@ -96,7 +96,7 @@ public function __construct() { $this->name = 'ps_facetedsearch'; $this->tab = 'front_office_features'; - $this->version = '3.13.2'; + $this->version = '3.14.0'; $this->author = 'PrestaShop'; $this->need_instance = 0; $this->bootstrap = true; @@ -172,6 +172,9 @@ protected function getDefaultFilters() 'label' => 'Product price filter (slider)', 'slider' => true, ], + 'layered_selection_extras' => [ + 'label' => 'Product extras filter', + ], ]; } @@ -216,7 +219,7 @@ public function install() $productsCount = $this->getDatabase()->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'product`'); if ($productsCount < static::LOCK_TEMPLATE_CREATION) { - $this->rebuildLayeredCache(); + $this->createDefaultTemplate(); } $this->rebuildPriceIndexTable(); @@ -591,7 +594,6 @@ public function indexProductPrices($idProduct, $smart = true) */ public function getContent() { - global $cookie; $message = ''; if (Tools::isSubmit('SubmitFilter')) { @@ -990,7 +992,7 @@ public function rebuildLayeredStructure() `controller` VARCHAR(64) NOT NULL, `id_category` INT(10) UNSIGNED NOT NULL, `id_value` INT(10) UNSIGNED NULL DEFAULT \'0\', - `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\') NOT NULL, + `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\',\'extras\') NOT NULL, `position` INT(10) UNSIGNED NOT NULL, `filter_type` int(10) UNSIGNED NOT NULL DEFAULT 0, `filter_show_limit` int(10) UNSIGNED NOT NULL DEFAULT 0, @@ -1027,17 +1029,25 @@ public function rebuildLayeredStructure() } /** - * Build layered cache - * - * @param array $productsIds - * @param array $categoriesIds - * @param bool $rebuildLayeredCategories + * This method creates the first initial filter after installing the module, + * from all available features and attributes. */ - public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $rebuildLayeredCategories = true) + public function createDefaultTemplate() { @set_time_limit(0); - $filterData = ['categories' => [], 'controllers' => ['category']]; + // Default filter data + $filterData = [ + 'categories' => [], + 'controllers' => [], + ]; + + // Add all stable controllers (except search) + foreach ($this->getSupportedControllers() as $controller_name => $data) { + if ($controller_name != 'search') { + $filterData['controllers'][] = $controller_name; + } + } /* Set memory limit to 128M only if current is lower */ $memoryLimit = Tools::getMemoryLimit(); @@ -1053,6 +1063,7 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb $joinProduct = Shop::addSqlAssociation('product', 'p'); $joinProductAttribute = Shop::addSqlAssociation('product_attribute', 'pa'); + // Fetch all available attributes and their values $attributeGroups = $this->query( 'SELECT a.id_attribute, a.id_attribute_group FROM ' . _DB_PREFIX_ . 'attribute a @@ -1062,10 +1073,8 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb ' . $joinProduct . $joinProductAttribute . ' LEFT JOIN ' . _DB_PREFIX_ . 'category_product cp ON (cp.id_product = p.id_product) LEFT JOIN ' . _DB_PREFIX_ . 'category c ON (c.id_category = cp.id_category) - WHERE c.active = 1' . - (count($categoriesIds) ? ' AND cp.id_category IN (' . implode(',', array_map('intval', $categoriesIds)) . ')' : '') . ' - AND ' . $alias . '.active = 1 AND ' . $alias . '.`visibility` IN ("both", "catalog") - ' . (count($productsIds) ? 'AND p.id_product IN (' . implode(',', array_map('intval', $productsIds)) . ')' : '') + WHERE c.active = 1 + AND ' . $alias . '.active = 1 AND ' . $alias . '.`visibility` IN ("both", "catalog")' ); $attributeGroupsById = []; @@ -1073,6 +1082,7 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb $attributeGroupsById[(int) $row['id_attribute']] = (int) $row['id_attribute_group']; } + // Fetch all available features and their values $features = $this->query( 'SELECT fv.id_feature_value, fv.id_feature FROM ' . _DB_PREFIX_ . 'feature_value fv @@ -1081,9 +1091,8 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb ' . $joinProduct . ' LEFT JOIN ' . _DB_PREFIX_ . 'category_product cp ON (cp.id_product = p.id_product) LEFT JOIN ' . _DB_PREFIX_ . 'category c ON (c.id_category = cp.id_category) - WHERE (fv.custom IS NULL OR fv.custom = 0) AND c.active = 1' . (count($categoriesIds) ? ' AND cp.id_category IN (' . implode(',', array_map('intval', $categoriesIds)) . ')' : '') . ' - AND ' . $alias . '.active = 1 AND ' . $alias . '.`visibility` IN ("both", "catalog") ' . - (count($productsIds) ? 'AND p.id_product IN (' . implode(',', array_map('intval', $productsIds)) . ')' : '') + WHERE (fv.custom IS NULL OR fv.custom = 0) AND c.active = 1 + AND ' . $alias . '.active = 1 AND ' . $alias . '.`visibility` IN ("both", "catalog") ' ); $featuresById = []; @@ -1104,10 +1113,9 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb LEFT JOIN ' . _DB_PREFIX_ . 'product_attribute pa ON (pa.id_product = p.id_product) ' . $joinProduct . $joinProductAttribute . ' LEFT JOIN ' . _DB_PREFIX_ . 'product_attribute_combination pac ON (pac.id_product_attribute = pa.id_product_attribute) - WHERE c.active = 1' . (count($categoriesIds) ? ' AND cp.id_category IN (' . implode(',', array_map('intval', $categoriesIds)) . ')' : '') . ' + WHERE c.active = 1 AND ' . $alias . '.active = 1 AND ' . $alias . '.`visibility` IN ("both", "catalog") - ' . (count($productsIds) ? 'AND p.id_product IN (' . implode(',', array_map('intval', $productsIds)) . ')' : '') . - ' AND (fv.custom IS NULL OR fv.custom = 0) + AND (fv.custom IS NULL OR fv.custom = 0) GROUP BY p.id_product' ); @@ -1138,11 +1146,36 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb if (!isset($nCategories[(int) $idCategory])) { $nCategories[(int) $idCategory] = 1; } + + // Stock filter + if (!isset($doneCategories[(int) $idCategory]['q'])) { + $filterData['layered_selection_stock'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; + $doneCategories[(int) $idCategory]['q'] = true; + $toInsert = true; + } + + // Add extras filter + if (!isset($doneCategories[(int) $idCategory]['e'])) { + $filterData['layered_selection_extras'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; + $doneCategories[(int) $idCategory]['e'] = true; + $toInsert = true; + } + + // Price filter + if (!isset($doneCategories[(int) $idCategory]['p'])) { + $filterData['layered_selection_price_slider'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; + $doneCategories[(int) $idCategory]['p'] = true; + $toInsert = true; + } + + // Category filter if (!isset($doneCategories[(int) $idCategory]['cat'])) { $filterData['layered_selection_subcategories'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; $doneCategories[(int) $idCategory]['cat'] = true; $toInsert = true; } + + // Attribute filter if (is_array($attributeGroupsById) && count($attributeGroupsById) > 0) { foreach ($a as $kAttribute => $attribute) { if (!isset($doneCategories[(int) $idCategory]['a' . (int) $attributeGroupsById[(int) $kAttribute]])) { @@ -1152,7 +1185,9 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb } } } - if (is_array($attributeGroupsById) && count($attributeGroupsById) > 0) { + + // Features filter + if (is_array($featuresById) && count($featuresById) > 0) { foreach ($f as $kFeature => $feature) { if (!isset($doneCategories[(int) $idCategory]['f' . (int) $featuresById[(int) $kFeature]])) { $filterData['layered_selection_feat_' . (int) $featuresById[(int) $kFeature]] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; @@ -1162,38 +1197,30 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb } } - if (!isset($doneCategories[(int) $idCategory]['q'])) { - $filterData['layered_selection_stock'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; - $doneCategories[(int) $idCategory]['q'] = true; - $toInsert = true; - } - + // Manufacturer filter if (!isset($doneCategories[(int) $idCategory]['m'])) { $filterData['layered_selection_manufacturer'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; $doneCategories[(int) $idCategory]['m'] = true; $toInsert = true; } + // Condition filter if (!isset($doneCategories[(int) $idCategory]['c'])) { $filterData['layered_selection_condition'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; $doneCategories[(int) $idCategory]['c'] = true; $toInsert = true; } + // Weight filter if (!isset($doneCategories[(int) $idCategory]['w'])) { $filterData['layered_selection_weight_slider'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; $doneCategories[(int) $idCategory]['w'] = true; $toInsert = true; } - - if (!isset($doneCategories[(int) $idCategory]['p'])) { - $filterData['layered_selection_price_slider'] = ['filter_type' => Converter::WIDGET_TYPE_CHECKBOX, 'filter_show_limit' => 0]; - $doneCategories[(int) $idCategory]['p'] = true; - $toInsert = true; - } } } + // If there are any filters available to setup, we will create the filter template if ($toInsert) { $this->getDatabase()->execute('INSERT INTO ' . _DB_PREFIX_ . 'layered_filter(name, filters, n_categories, date_add) VALUES (\'' . sprintf($this->trans('My template %s', [], 'Modules.Facetedsearch.Admin'), date('Y-m-d')) . '\', \'' . pSQL(serialize($filterData)) . '\', ' . count($filterData['categories']) . ', NOW())'); @@ -1204,11 +1231,10 @@ public function rebuildLayeredCache($productsIds = [], $categoriesIds = [], $reb $this->getDatabase()->execute('INSERT INTO ' . _DB_PREFIX_ . 'layered_filter_shop (`id_layered_filter`, `id_shop`) VALUES(' . $last_id . ', ' . (int) $idShop . ')'); } - - if ($rebuildLayeredCategories) { - $this->buildLayeredCategories(); - } } + + // Now we need to build layered_category table from this template + $this->buildLayeredCategories(); } /** @@ -1295,6 +1321,8 @@ public function buildLayeredCategories() } elseif (substr($key, 0, 23) == 'layered_selection_feat_') { $sqlInsert .= '(' . (int) $idCategory . ', \'' . $controller . '\', ' . (int) $idShop . ', ' . (int) str_replace('layered_selection_feat_', '', $key) . ', \'id_feature\',' . (int) $n . ', ' . (int) $limit . ', ' . (int) $type . '),'; + } elseif ($key == 'layered_selection_extras') { + $sqlInsert .= '(' . (int) $idCategory . ', \'' . $controller . '\', ' . (int) $idShop . ', NULL,\'extras\',' . (int) $n . ', ' . (int) $limit . ', ' . (int) $type . '),'; } ++$nbSqlValuesToInsert; diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index cb13a4d66..821b62812 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -98,20 +98,20 @@ public function getQuery() // Process and generate all fields for the SQL query below $orderField = $this->computeOrderByField($filterToTableMapping); + $selectFields = $this->computeSelectFields($filterToTableMapping); + $whereConditions = $this->computeWhereConditions($filterToTableMapping); + $joinConditions = $this->computeJoinConditions($filterToTableMapping); + $groupFields = $this->computeGroupByFields($filterToTableMapping); // Now, let's build the query... // If this query IS the initial population (the base table), we are selecting from product table if ($this->getInitialPopulation() === null) { $referenceTable = _DB_PREFIX_ . 'product'; + // If not, we will call this function again but for the initial population } else { $referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')'; } - $selectFields = $this->computeSelectFields($filterToTableMapping); - $whereConditions = $this->computeWhereConditions($filterToTableMapping); - $joinConditions = $this->computeJoinConditions($filterToTableMapping); - $groupFields = $this->computeGroupByFields($filterToTableMapping); - $query = 'SELECT ' . implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p'; foreach ($joinConditions as $joinAliasInfos) { @@ -327,7 +327,7 @@ protected function getFieldMapping() (sp.from = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' >= sp.from) AND (sp.to = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' <= sp.to) )', - 'joinType' => self::INNER_JOIN, + 'joinType' => self::LEFT_JOIN, ], ]; @@ -805,6 +805,8 @@ public function useFiltersAsInitialPopulation() 'weight', 'price', 'sales', + 'on_sale', + 'date_add', ] ); diff --git a/src/Filters/Block.php b/src/Filters/Block.php index bd411ff84..d9924de81 100644 --- a/src/Filters/Block.php +++ b/src/Filters/Block.php @@ -139,6 +139,9 @@ public function getFilterBlock( case 'availability': $filterBlocks[] = $this->getAvailabilitiesBlock($filter, $selectedFilters); break; + case 'extras': + $filterBlocks[] = $this->getHighlightsBlock($filter, $selectedFilters); + break; case 'manufacturer': $filterBlocks[] = $this->getManufacturersBlock($filter, $selectedFilters, $idLang); break; @@ -532,6 +535,99 @@ private function getAvailabilitiesBlock($filter, $selectedFilters) return $quantityBlock; } + /** + * Gets block for extra product properties like "new", "on sale" and "discounted" + * + * @param array $filter + * @param array $selectedFilters + * + * @return array + */ + private function getHighlightsBlock($filter, $selectedFilters) + { + // Prepare array with options + $extrasOptions = []; + + // Products on sale - available everywhere + $extrasOptions['sale'] = [ + 'name' => $this->context->getTranslator()->trans( + 'On sale', + [], + 'Modules.Facetedsearch.Shop' + ), + 'nbr' => 0, + ]; + $filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter(Search::HIGHLIGHTS_FILTER); + $filteredSearchAdapter->addOperationsFilter( + Search::HIGHLIGHTS_FILTER, + [[['on_sale', [1], '=']]] + ); + $extrasOptions['sale']['nbr'] = $filteredSearchAdapter->count(); + + // New products - available everywhere except that page + if ($this->query->getQueryType() != 'new-products') { + $extrasOptions['new'] = [ + 'name' => $this->context->getTranslator()->trans( + 'New product', + [], + 'Modules.Facetedsearch.Shop' + ), + 'nbr' => 0, + ]; + $filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('date_add'); + $timeCondition = date( + 'Y-m-d 00:00:00', + strtotime( + ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') > 0 ? + '-' . ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') - 1) . ' days' : + '+ 1 days') + ) + ); + $filteredSearchAdapter->addFilter('date_add', ["'" . $timeCondition . "'"], '>'); + $extrasOptions['new']['nbr'] = $filteredSearchAdapter->count(); + } + + // Discounted products - available everywhere except that page + if ($this->query->getQueryType() != 'prices-drop') { + $extrasOptions['discount'] = [ + 'name' => $this->context->getTranslator()->trans( + 'Discounted', + [], + 'Modules.Facetedsearch.Shop' + ), + 'nbr' => 0, + ]; + $filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter(Search::HIGHLIGHTS_FILTER); + $filteredSearchAdapter->addOperationsFilter( + Search::HIGHLIGHTS_FILTER, + [[['reduction', [0], '>']]] + ); + $extrasOptions['discount']['nbr'] = $filteredSearchAdapter->count(); + } + + // If some filters are selected, we mark them as such + if (isset($selectedFilters['extras'])) { + // We loop through selected filters and assign it to our options and remove the rest + foreach ($extrasOptions as $key => $values) { + if (in_array($key, $selectedFilters['extras'], true)) { + $extrasOptions[$key]['checked'] = true; + } + } + } + + $conditionBlock = [ + 'type_lite' => 'extras', + 'type' => 'extras', + 'id_key' => 0, + 'name' => $this->context->getTranslator()->trans('Selections', [], 'Modules.Facetedsearch.Shop'), + 'values' => $extrasOptions, + 'filter_show_limit' => (int) $filter['filter_show_limit'], + 'filter_type' => $filter['filter_type'], + ]; + + return $conditionBlock; + } + /** * Get the manufacturers filter block * diff --git a/src/Filters/Converter.php b/src/Filters/Converter.php index abe255c03..549d2d2f9 100644 --- a/src/Filters/Converter.php +++ b/src/Filters/Converter.php @@ -47,6 +47,7 @@ class Converter const TYPE_MANUFACTURER = 'manufacturer'; const TYPE_PRICE = 'price'; const TYPE_WEIGHT = 'weight'; + const TYPE_EXTRAS = 'extras'; const PROPERTY_URL_NAME = 'url_name'; const PROPERTY_COLOR = 'color'; @@ -115,6 +116,7 @@ public function getFacetsFromFilterBlocks(array $filterBlocks) switch ($filterBlock['type']) { case self::TYPE_CATEGORY: case self::TYPE_CONDITION: + case self::TYPE_EXTRAS: case self::TYPE_MANUFACTURER: case self::TYPE_AVAILABILITY: case self::TYPE_ATTRIBUTE_GROUP: @@ -351,6 +353,37 @@ public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query) } } break; + case self::TYPE_EXTRAS: + if (!isset($receivedFilters[$filterLabel])) { + // No need to filter if no information + continue 2; + } + + $extrasOptions = [ + $this->context->getTranslator()->trans( + 'New product', + [], + 'Modules.Facetedsearch.Shop' + ) => 'new', + $this->context->getTranslator()->trans( + 'On sale', + [], + 'Modules.Facetedsearch.Shop' + ) => 'sale', + $this->context->getTranslator()->trans( + 'Discounted', + [], + 'Modules.Facetedsearch.Shop' + ) => 'discount', + ]; + + $searchFilters[$filter['type']] = []; + foreach ($extrasOptions as $extrasOption => $optionId) { + if (isset($receivedFilters[$filterLabel]) && in_array($extrasOption, $receivedFilters[$filterLabel])) { + $searchFilters[$filter['type']][] = $optionId; + } + } + break; case self::TYPE_FEATURE: $features = $this->dataAccessor->getFeatures($idLang); foreach ($features as $feature) { @@ -471,6 +504,8 @@ private function convertFilterTypeToLabel($filterType) return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop'); case self::TYPE_CONDITION: return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop'); + case self::TYPE_EXTRAS: + return $this->context->getTranslator()->trans('Selections', [], 'Modules.Facetedsearch.Shop'); case self::TYPE_AVAILABILITY: return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop'); case self::TYPE_MANUFACTURER: diff --git a/src/Product/Search.php b/src/Product/Search.php index 638057446..8e85c9e2b 100644 --- a/src/Product/Search.php +++ b/src/Product/Search.php @@ -33,6 +33,7 @@ class Search { const STOCK_MANAGEMENT_FILTER = 'with_stock_management'; + const HIGHLIGHTS_FILTER = 'extras'; /** * @var bool @@ -173,6 +174,41 @@ private function addSearchFilters($selectedFilters) $this->addFilter('id_category', $filterValues); break; + case 'extras': + // Filter for new products + if (in_array('new', $filterValues)) { + $timeCondition = date( + 'Y-m-d 00:00:00', + strtotime( + ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') > 0 ? + '-' . ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') - 1) . ' days' : + '+ 1 days') + ) + ); + // Reset filter to prevent two same filters if we are on new products page + $this->getSearchAdapter()->addFilter('date_add', ["'" . $timeCondition . "'"], '>'); + } + + // Filter for discounts - they must work as OR + $operationsFilter = []; + if (in_array('discount', $filterValues)) { + $operationsFilter[] = [ + ['reduction', [0], '>'], + ]; + } + if (in_array('sale', $filterValues)) { + $operationsFilter[] = [ + ['on_sale', [1], '='], + ]; + } + if (!empty($operationsFilter)) { + $this->getSearchAdapter()->addOperationsFilter( + self::HIGHLIGHTS_FILTER, + $operationsFilter + ); + } + break; + case 'availability': /* * $filterValues options can have following values: @@ -360,6 +396,11 @@ private function addControllerSpecificFilters() * If there is a zero set to disable this feature, it creates unreachable condition. */ if ($this->query->getQueryType() == 'new-products') { + // We check if some specific filter of this type wasn't added before + if (!empty($this->getSearchAdapter()->getFilter('date_add'))) { + return; + } + $timeCondition = date( 'Y-m-d 00:00:00', strtotime( @@ -386,6 +427,11 @@ private function addControllerSpecificFilters() * We are selecting products that have a specific price created meeting certain conditions. */ if ($this->query->getQueryType() == 'prices-drop') { + // We check if some specific filter of this type wasn't added before + if (!empty($this->getSearchAdapter()->getFilter('reduction'))) { + return; + } + $this->getSearchAdapter()->addFilter('reduction', [0], '>'); } diff --git a/src/Product/SearchProvider.php b/src/Product/SearchProvider.php index d07108d74..b42e439ae 100644 --- a/src/Product/SearchProvider.php +++ b/src/Product/SearchProvider.php @@ -507,7 +507,7 @@ private function addEncodedFacetsToFilters(array $facets) /** * Remove the facet when there's only 1 result. * Keep facet status when it's a slider. - * Keep facet status if it's a availability facet. + * Keep facet status if it's a availability or extras facet. * * @param array $facets * @param int $totalProducts @@ -545,8 +545,8 @@ private function hideUselessFacets(array $facets, $totalProducts) && $usefulFiltersCount > 0 ) || - // If there is only one filter, but it's availability filter - we want this one to be displayed all the time - ($usefulFiltersCount === 1 && $facet->getType() == 'availability') + // If there is only one filter, but it's availability or extras filter - we want this one to be displayed all the time + ($usefulFiltersCount === 1 && ($facet->getType() == 'availability' || $facet->getType() == 'extras')) ); // Other cases - hidden by default } diff --git a/tests/php/FacetedSearch/Adapter/MySQLTest.php b/tests/php/FacetedSearch/Adapter/MySQLTest.php index a8a9c641d..85f95f0c2 100644 --- a/tests/php/FacetedSearch/Adapter/MySQLTest.php +++ b/tests/php/FacetedSearch/Adapter/MySQLTest.php @@ -207,7 +207,7 @@ public function testValueCountWithInitialPopulation() $dbInstanceMock = Mockery::mock(Db::class); $dbInstanceMock->shouldReceive('executeS') ->once() - ->with('SELECT p.id_product, p.weight, COUNT(DISTINCT p.id_product) c FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product)) p GROUP BY p.weight') + ->with('SELECT p.id_product, p.weight, COUNT(DISTINCT p.id_product) c FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product)) p GROUP BY p.weight') ->andReturn( [ [ @@ -244,7 +244,7 @@ public function testValueCountWithInitialPopulationAndStockManagement() $dbInstanceMock = Mockery::mock(Db::class); $dbInstanceMock->shouldReceive('executeS') ->once() - ->with('SELECT p.id_product, p.weight, COUNT(DISTINCT p.id_product) c FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) WHERE ((sa.quantity>=0))) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) WHERE ((sa.quantity>=0)) GROUP BY p.weight') + ->with('SELECT p.id_product, p.weight, COUNT(DISTINCT p.id_product) c FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) WHERE ((sa.quantity>=0))) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) WHERE ((sa.quantity>=0)) GROUP BY p.weight') ->andReturn( [ [ @@ -391,7 +391,7 @@ public function testGetQueryWithPriceOrderFieldInAscWithInitialPopulation() $this->adapter->setOrderDirection('asc'); $this->assertEquals( - 'SELECT p.id_product FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, m.name FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) LEFT JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer)) p LEFT JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer) ORDER BY m.name ASC, p.id_product DESC', + 'SELECT p.id_product FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add, m.name FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) LEFT JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer)) p LEFT JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer) ORDER BY m.name ASC, p.id_product DESC', $this->adapter->getQuery() ); } @@ -404,7 +404,7 @@ public function testGetQueryWithPositionOrderFieldInAscWithInitialPopulation() $this->adapter->setOrderDirection('desc'); $this->assertEquals( - 'SELECT p.id_product FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY p.position DESC, p.id_product DESC', + 'SELECT p.id_product FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY p.position DESC, p.id_product DESC', $this->adapter->getQuery() ); } @@ -428,7 +428,7 @@ public function testGetQueryWithComputeShowLastEnabled() $this->adapter->setOrderDirection('desc'); $this->assertEquals( - 'SELECT p.id_product, sa.out_of_stock FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY IFNULL(p.quantity, 0) <= 0, IFNULL(p.quantity, 0) <= 0 AND FIELD(sa.out_of_stock, 0) ASC, p.position DESC, p.id_product DESC', + 'SELECT p.id_product, sa.out_of_stock FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY IFNULL(p.quantity, 0) <= 0, IFNULL(p.quantity, 0) <= 0 AND FIELD(sa.out_of_stock, 0) ASC, p.position DESC, p.id_product DESC', $this->adapter->getQuery() ); } @@ -452,7 +452,7 @@ public function testGetQueryWithComputeShowLastEnabledAndDenyOrderOutOfStockProd $this->adapter->setOrderDirection('desc'); $this->assertEquals( - 'SELECT p.id_product, sa.out_of_stock FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY IFNULL(p.quantity, 0) <= 0, IFNULL(p.quantity, 0) <= 0 AND FIELD(sa.out_of_stock, 1) DESC, p.position DESC, p.id_product DESC', + 'SELECT p.id_product, sa.out_of_stock FROM (SELECT p.id_product, p.id_manufacturer, SUM(sa.quantity) as quantity, p.condition, p.weight, p.price, psales.quantity as sales, p.on_sale, p.date_add, cp.position FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) LEFT JOIN ps_product_sale psales ON (psales.id_product = p.id_product) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product)) p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) LEFT JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY IFNULL(p.quantity, 0) <= 0, IFNULL(p.quantity, 0) <= 0 AND FIELD(sa.out_of_stock, 1) DESC, p.position DESC, p.id_product DESC', $this->adapter->getQuery() ); } diff --git a/upgrade/upgrade-3.14.0.php b/upgrade/upgrade-3.14.0.php new file mode 100644 index 000000000..494d5bf97 --- /dev/null +++ b/upgrade/upgrade-3.14.0.php @@ -0,0 +1,32 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +if (!defined('_PS_VERSION_')) { + exit; +} + +function upgrade_module_3_14_0(Ps_Facetedsearch $module) +{ + // Add availabilility to allowed types + Db::getInstance()->execute( + 'ALTER TABLE `' . _DB_PREFIX_ . 'layered_category` + CHANGE `type` `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\',\'extras\') NOT NULL;'); + + return true; +} diff --git a/views/templates/admin/add.tpl b/views/templates/admin/add.tpl index 4096abad9..f6bc62ed6 100644 --- a/views/templates/admin/add.tpl +++ b/views/templates/admin/add.tpl @@ -111,6 +111,36 @@ + +
  • +
    + +
    +
    + {l s='Product extras filter' d='Modules.Facetedsearch.Admin'} +
    +
    + +
    + {call get_limit_select element="layered_selection_extras"} +
    +
    +
    + +
    + +

    {l s='Checkbox' d='Modules.Facetedsearch.Admin'}

    +
    +
    +
  • +